🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

setuptools

Package Overview
Dependencies
Maintainers
3
Versions
624
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

setuptools - pypi Package Compare versions

Comparing version
80.9.0
to
75.3.3
+34
setuptools/_distutils/compat/py38.py
import sys
if sys.version_info < (3, 9):
def removesuffix(self, suffix):
# suffix='' should not call self[:-0].
if suffix and self.endswith(suffix):
return self[: -len(suffix)]
else:
return self[:]
def removeprefix(self, prefix):
if self.startswith(prefix):
return self[len(prefix) :]
else:
return self[:]
else:
def removesuffix(self, suffix):
return self.removesuffix(suffix)
def removeprefix(self, prefix):
return self.removeprefix(prefix)
def aix_platform(osname, version, release):
try:
import _aix_support # type: ignore
return _aix_support.aix_platform()
except ImportError:
pass
return f"{osname}-{version}.{release}"
# flake8: noqa
import contextlib
import builtins
import sys
from test.support import requires_zlib
import test.support
ModuleNotFoundError = getattr(builtins, 'ModuleNotFoundError', ImportError)
try:
from test.support.warnings_helper import check_warnings
except (ModuleNotFoundError, ImportError):
from test.support import check_warnings
try:
from test.support.os_helper import (
rmtree,
EnvironmentVarGuard,
unlink,
skip_unless_symlink,
temp_dir,
)
except (ModuleNotFoundError, ImportError):
from test.support import (
rmtree,
EnvironmentVarGuard,
unlink,
skip_unless_symlink,
temp_dir,
)
try:
from test.support.import_helper import (
DirsOnSysPath,
CleanImport,
)
except (ModuleNotFoundError, ImportError):
from test.support import (
DirsOnSysPath,
CleanImport,
)
if sys.version_info < (3, 9):
requires_zlib = lambda: test.support.requires_zlib
import os
import platform
import sys
import sysconfig
import textwrap
from distutils import ccompiler
import pytest
def _make_strs(paths):
"""
Convert paths to strings for legacy compatibility.
"""
if sys.version_info > (3, 8) and platform.system() != "Windows":
return paths
return list(map(os.fspath, paths))
@pytest.fixture
def c_file(tmp_path):
c_file = tmp_path / 'foo.c'
gen_headers = ('Python.h',)
is_windows = platform.system() == "Windows"
plat_headers = ('windows.h',) * is_windows
all_headers = gen_headers + plat_headers
headers = '\n'.join(f'#include <{header}>\n' for header in all_headers)
payload = (
textwrap.dedent(
"""
#headers
void PyInit_foo(void) {}
"""
)
.lstrip()
.replace('#headers', headers)
)
c_file.write_text(payload, encoding='utf-8')
return c_file
def test_set_include_dirs(c_file):
"""
Extensions should build even if set_include_dirs is invoked.
In particular, compiler-specific paths should not be overridden.
"""
compiler = ccompiler.new_compiler()
python = sysconfig.get_paths()['include']
compiler.set_include_dirs([python])
compiler.compile(_make_strs([c_file]))
# do it again, setting include dirs after any initialization
compiler.set_include_dirs([python])
compiler.compile(_make_strs([c_file]))
def test_has_function_prototype():
# Issue https://github.com/pypa/setuptools/issues/3648
# Test prototype-generating behavior.
compiler = ccompiler.new_compiler()
# Every C implementation should have these.
assert compiler.has_function('abort')
assert compiler.has_function('exit')
with pytest.deprecated_call(match='includes is deprecated'):
# abort() is a valid expression with the <stdlib.h> prototype.
assert compiler.has_function('abort', includes=['stdlib.h'])
with pytest.deprecated_call(match='includes is deprecated'):
# But exit() is not valid with the actual prototype in scope.
assert not compiler.has_function('exit', includes=['stdlib.h'])
# And setuptools_does_not_exist is not declared or defined at all.
assert not compiler.has_function('setuptools_does_not_exist')
with pytest.deprecated_call(match='includes is deprecated'):
assert not compiler.has_function(
'setuptools_does_not_exist', includes=['stdio.h']
)
def test_include_dirs_after_multiple_compile_calls(c_file):
"""
Calling compile multiple times should not change the include dirs
(regression test for setuptools issue #3591).
"""
compiler = ccompiler.new_compiler()
python = sysconfig.get_paths()['include']
compiler.set_include_dirs([python])
compiler.compile(_make_strs([c_file]))
assert compiler.include_dirs == [python]
compiler.compile(_make_strs([c_file]))
assert compiler.include_dirs == [python]
"""Tests for distutils.cygwinccompiler."""
import os
import sys
from distutils import sysconfig
from distutils.cygwinccompiler import (
CONFIG_H_NOTOK,
CONFIG_H_OK,
CONFIG_H_UNCERTAIN,
check_config_h,
get_msvcr,
)
from distutils.tests import support
import pytest
@pytest.fixture(autouse=True)
def stuff(request, monkeypatch, distutils_managed_tempdir):
self = request.instance
self.python_h = os.path.join(self.mkdtemp(), 'python.h')
monkeypatch.setattr(sysconfig, 'get_config_h_filename', self._get_config_h_filename)
monkeypatch.setattr(sys, 'version', sys.version)
class TestCygwinCCompiler(support.TempdirManager):
def _get_config_h_filename(self):
return self.python_h
@pytest.mark.skipif('sys.platform != "cygwin"')
@pytest.mark.skipif('not os.path.exists("/usr/lib/libbash.dll.a")')
def test_find_library_file(self):
from distutils.cygwinccompiler import CygwinCCompiler
compiler = CygwinCCompiler()
link_name = "bash"
linkable_file = compiler.find_library_file(["/usr/lib"], link_name)
assert linkable_file is not None
assert os.path.exists(linkable_file)
assert linkable_file == f"/usr/lib/lib{link_name:s}.dll.a"
@pytest.mark.skipif('sys.platform != "cygwin"')
def test_runtime_library_dir_option(self):
from distutils.cygwinccompiler import CygwinCCompiler
compiler = CygwinCCompiler()
assert compiler.runtime_library_dir_option('/foo') == []
def test_check_config_h(self):
# check_config_h looks for "GCC" in sys.version first
# returns CONFIG_H_OK if found
sys.version = (
'2.6.1 (r261:67515, Dec 6 2008, 16:42:21) \n[GCC '
'4.0.1 (Apple Computer, Inc. build 5370)]'
)
assert check_config_h()[0] == CONFIG_H_OK
# then it tries to see if it can find "__GNUC__" in pyconfig.h
sys.version = 'something without the *CC word'
# if the file doesn't exist it returns CONFIG_H_UNCERTAIN
assert check_config_h()[0] == CONFIG_H_UNCERTAIN
# if it exists but does not contain __GNUC__, it returns CONFIG_H_NOTOK
self.write_file(self.python_h, 'xxx')
assert check_config_h()[0] == CONFIG_H_NOTOK
# and CONFIG_H_OK if __GNUC__ is found
self.write_file(self.python_h, 'xxx __GNUC__ xxx')
assert check_config_h()[0] == CONFIG_H_OK
def test_get_msvcr(self):
assert get_msvcr() == []
@pytest.mark.skipif('sys.platform != "cygwin"')
def test_dll_libraries_not_none(self):
from distutils.cygwinccompiler import CygwinCCompiler
compiler = CygwinCCompiler()
assert compiler.dll_libraries is not None
from distutils import sysconfig
from distutils.errors import CCompilerError, DistutilsPlatformError
from distutils.util import is_mingw, split_quoted
import pytest
class TestMingw32CCompiler:
@pytest.mark.skipif(not is_mingw(), reason='not on mingw')
def test_compiler_type(self):
from distutils.cygwinccompiler import Mingw32CCompiler
compiler = Mingw32CCompiler()
assert compiler.compiler_type == 'mingw32'
@pytest.mark.skipif(not is_mingw(), reason='not on mingw')
def test_set_executables(self, monkeypatch):
from distutils.cygwinccompiler import Mingw32CCompiler
monkeypatch.setenv('CC', 'cc')
monkeypatch.setenv('CXX', 'c++')
compiler = Mingw32CCompiler()
assert compiler.compiler == split_quoted('cc -O -Wall')
assert compiler.compiler_so == split_quoted('cc -shared -O -Wall')
assert compiler.compiler_cxx == split_quoted('c++ -O -Wall')
assert compiler.linker_exe == split_quoted('cc')
assert compiler.linker_so == split_quoted('cc -shared')
@pytest.mark.skipif(not is_mingw(), reason='not on mingw')
def test_runtime_library_dir_option(self):
from distutils.cygwinccompiler import Mingw32CCompiler
compiler = Mingw32CCompiler()
with pytest.raises(DistutilsPlatformError):
compiler.runtime_library_dir_option('/usr/lib')
@pytest.mark.skipif(not is_mingw(), reason='not on mingw')
def test_cygwincc_error(self, monkeypatch):
import distutils.cygwinccompiler
monkeypatch.setattr(distutils.cygwinccompiler, 'is_cygwincc', lambda _: True)
with pytest.raises(CCompilerError):
distutils.cygwinccompiler.Mingw32CCompiler()
@pytest.mark.skipif('sys.platform == "cygwin"')
def test_customize_compiler_with_msvc_python(self):
from distutils.cygwinccompiler import Mingw32CCompiler
# In case we have an MSVC Python build, but still want to use
# Mingw32CCompiler, then customize_compiler() shouldn't fail at least.
# https://github.com/pypa/setuptools/issues/4456
compiler = Mingw32CCompiler()
sysconfig.customize_compiler(compiler)
"""Tests for distutils._msvccompiler."""
import os
import sys
import sysconfig
import threading
import unittest.mock as mock
from distutils import _msvccompiler
from distutils.errors import DistutilsPlatformError
from distutils.tests import support
from distutils.util import get_platform
import pytest
needs_winreg = pytest.mark.skipif('not hasattr(_msvccompiler, "winreg")')
class Testmsvccompiler(support.TempdirManager):
def test_no_compiler(self, monkeypatch):
# makes sure query_vcvarsall raises
# a DistutilsPlatformError if the compiler
# is not found
def _find_vcvarsall(plat_spec):
return None, None
monkeypatch.setattr(_msvccompiler, '_find_vcvarsall', _find_vcvarsall)
with pytest.raises(DistutilsPlatformError):
_msvccompiler._get_vc_env(
'wont find this version',
)
@pytest.mark.skipif(
not sysconfig.get_platform().startswith("win"),
reason="Only run test for non-mingw Windows platforms",
)
@pytest.mark.parametrize(
"plat_name, expected",
[
("win-arm64", "win-arm64"),
("win-amd64", "win-amd64"),
(None, get_platform()),
],
)
def test_cross_platform_compilation_paths(self, monkeypatch, plat_name, expected):
"""
Ensure a specified target platform is passed to _get_vcvars_spec.
"""
compiler = _msvccompiler.MSVCCompiler()
def _get_vcvars_spec(host_platform, platform):
assert platform == expected
monkeypatch.setattr(_msvccompiler, '_get_vcvars_spec', _get_vcvars_spec)
compiler.initialize(plat_name)
@needs_winreg
def test_get_vc_env_unicode(self):
test_var = 'ṰḖṤṪ┅ṼẨṜ'
test_value = '₃⁴₅'
# Ensure we don't early exit from _get_vc_env
old_distutils_use_sdk = os.environ.pop('DISTUTILS_USE_SDK', None)
os.environ[test_var] = test_value
try:
env = _msvccompiler._get_vc_env('x86')
assert test_var.lower() in env
assert test_value == env[test_var.lower()]
finally:
os.environ.pop(test_var)
if old_distutils_use_sdk:
os.environ['DISTUTILS_USE_SDK'] = old_distutils_use_sdk
@needs_winreg
@pytest.mark.parametrize('ver', (2015, 2017))
def test_get_vc(self, ver):
# This function cannot be mocked, so pass if VC is found
# and skip otherwise.
lookup = getattr(_msvccompiler, f'_find_vc{ver}')
expected_version = {2015: 14, 2017: 15}[ver]
version, path = lookup()
if not version:
pytest.skip(f"VS {ver} is not installed")
assert version >= expected_version
assert os.path.isdir(path)
class CheckThread(threading.Thread):
exc_info = None
def run(self):
try:
super().run()
except Exception:
self.exc_info = sys.exc_info()
def __bool__(self):
return not self.exc_info
class TestSpawn:
def test_concurrent_safe(self):
"""
Concurrent calls to spawn should have consistent results.
"""
compiler = _msvccompiler.MSVCCompiler()
compiler._paths = "expected"
inner_cmd = 'import os; assert os.environ["PATH"] == "expected"'
command = [sys.executable, '-c', inner_cmd]
threads = [
CheckThread(target=compiler.spawn, args=[command]) for n in range(100)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert all(threads)
def test_concurrent_safe_fallback(self):
"""
If CCompiler.spawn has been monkey-patched without support
for an env, it should still execute.
"""
from distutils import ccompiler
compiler = _msvccompiler.MSVCCompiler()
compiler._paths = "expected"
def CCompiler_spawn(self, cmd):
"A spawn without an env argument."
assert os.environ["PATH"] == "expected"
with mock.patch.object(ccompiler.CCompiler, 'spawn', CCompiler_spawn):
compiler.spawn(["n/a"])
assert os.environ.get("PATH") != "expected"
"""Tests for distutils.unixccompiler."""
import os
import sys
import unittest.mock as mock
from distutils import sysconfig
from distutils.compat import consolidate_linker_args
from distutils.errors import DistutilsPlatformError
from distutils.unixccompiler import UnixCCompiler
from distutils.util import _clear_cached_macosx_ver
import pytest
from . import support
from .compat.py38 import EnvironmentVarGuard
@pytest.fixture(autouse=True)
def save_values(monkeypatch):
monkeypatch.setattr(sys, 'platform', sys.platform)
monkeypatch.setattr(sysconfig, 'get_config_var', sysconfig.get_config_var)
monkeypatch.setattr(sysconfig, 'get_config_vars', sysconfig.get_config_vars)
@pytest.fixture(autouse=True)
def compiler_wrapper(request):
class CompilerWrapper(UnixCCompiler):
def rpath_foo(self):
return self.runtime_library_dir_option('/foo')
request.instance.cc = CompilerWrapper()
class TestUnixCCompiler(support.TempdirManager):
@pytest.mark.skipif('platform.system == "Windows"')
def test_runtime_libdir_option(self): # noqa: C901
# Issue #5900; GitHub Issue #37
#
# Ensure RUNPATH is added to extension modules with RPATH if
# GNU ld is used
# darwin
sys.platform = 'darwin'
darwin_ver_var = 'MACOSX_DEPLOYMENT_TARGET'
darwin_rpath_flag = '-Wl,-rpath,/foo'
darwin_lib_flag = '-L/foo'
# (macOS version from syscfg, macOS version from env var) -> flag
# Version value of None generates two tests: as None and as empty string
# Expected flag value of None means an mismatch exception is expected
darwin_test_cases = [
((None, None), darwin_lib_flag),
((None, '11'), darwin_rpath_flag),
(('10', None), darwin_lib_flag),
(('10.3', None), darwin_lib_flag),
(('10.3.1', None), darwin_lib_flag),
(('10.5', None), darwin_rpath_flag),
(('10.5.1', None), darwin_rpath_flag),
(('10.3', '10.3'), darwin_lib_flag),
(('10.3', '10.5'), darwin_rpath_flag),
(('10.5', '10.3'), darwin_lib_flag),
(('10.5', '11'), darwin_rpath_flag),
(('10.4', '10'), None),
]
def make_darwin_gcv(syscfg_macosx_ver):
def gcv(var):
if var == darwin_ver_var:
return syscfg_macosx_ver
return "xxx"
return gcv
def do_darwin_test(syscfg_macosx_ver, env_macosx_ver, expected_flag):
env = os.environ
msg = f"macOS version = (sysconfig={syscfg_macosx_ver!r}, env={env_macosx_ver!r})"
# Save
old_gcv = sysconfig.get_config_var
old_env_macosx_ver = env.get(darwin_ver_var)
# Setup environment
_clear_cached_macosx_ver()
sysconfig.get_config_var = make_darwin_gcv(syscfg_macosx_ver)
if env_macosx_ver is not None:
env[darwin_ver_var] = env_macosx_ver
elif darwin_ver_var in env:
env.pop(darwin_ver_var)
# Run the test
if expected_flag is not None:
assert self.cc.rpath_foo() == expected_flag, msg
else:
with pytest.raises(
DistutilsPlatformError, match=darwin_ver_var + r' mismatch'
):
self.cc.rpath_foo()
# Restore
if old_env_macosx_ver is not None:
env[darwin_ver_var] = old_env_macosx_ver
elif darwin_ver_var in env:
env.pop(darwin_ver_var)
sysconfig.get_config_var = old_gcv
_clear_cached_macosx_ver()
for macosx_vers, expected_flag in darwin_test_cases:
syscfg_macosx_ver, env_macosx_ver = macosx_vers
do_darwin_test(syscfg_macosx_ver, env_macosx_ver, expected_flag)
# Bonus test cases with None interpreted as empty string
if syscfg_macosx_ver is None:
do_darwin_test("", env_macosx_ver, expected_flag)
if env_macosx_ver is None:
do_darwin_test(syscfg_macosx_ver, "", expected_flag)
if syscfg_macosx_ver is None and env_macosx_ver is None:
do_darwin_test("", "", expected_flag)
old_gcv = sysconfig.get_config_var
# hp-ux
sys.platform = 'hp-ux'
def gcv(v):
return 'xxx'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == ['+s', '-L/foo']
def gcv(v):
return 'gcc'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == ['-Wl,+s', '-L/foo']
def gcv(v):
return 'g++'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == ['-Wl,+s', '-L/foo']
sysconfig.get_config_var = old_gcv
# GCC GNULD
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'gcc'
elif v == 'GNULD':
return 'yes'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == consolidate_linker_args([
'-Wl,--enable-new-dtags',
'-Wl,-rpath,/foo',
])
def gcv(v):
if v == 'CC':
return 'gcc -pthread -B /bar'
elif v == 'GNULD':
return 'yes'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == consolidate_linker_args([
'-Wl,--enable-new-dtags',
'-Wl,-rpath,/foo',
])
# GCC non-GNULD
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'gcc'
elif v == 'GNULD':
return 'no'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == '-Wl,-R/foo'
# GCC GNULD with fully qualified configuration prefix
# see #7617
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'x86_64-pc-linux-gnu-gcc-4.4.2'
elif v == 'GNULD':
return 'yes'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == consolidate_linker_args([
'-Wl,--enable-new-dtags',
'-Wl,-rpath,/foo',
])
# non-GCC GNULD
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'cc'
elif v == 'GNULD':
return 'yes'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == consolidate_linker_args([
'-Wl,--enable-new-dtags',
'-Wl,-rpath,/foo',
])
# non-GCC non-GNULD
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'cc'
elif v == 'GNULD':
return 'no'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == '-Wl,-R/foo'
@pytest.mark.skipif('platform.system == "Windows"')
def test_cc_overrides_ldshared(self):
# Issue #18080:
# ensure that setting CC env variable also changes default linker
def gcv(v):
if v == 'LDSHARED':
return 'gcc-4.2 -bundle -undefined dynamic_lookup '
return 'gcc-4.2'
def gcvs(*args, _orig=sysconfig.get_config_vars):
if args:
return list(map(sysconfig.get_config_var, args))
return _orig()
sysconfig.get_config_var = gcv
sysconfig.get_config_vars = gcvs
with EnvironmentVarGuard() as env:
env['CC'] = 'my_cc'
del env['LDSHARED']
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so[0] == 'my_cc'
@pytest.mark.skipif('platform.system == "Windows"')
@pytest.mark.usefixtures('disable_macos_customization')
def test_cc_overrides_ldshared_for_cxx_correctly(self):
"""
Ensure that setting CC env variable also changes default linker
correctly when building C++ extensions.
pypa/distutils#126
"""
def gcv(v):
if v == 'LDSHARED':
return 'gcc-4.2 -bundle -undefined dynamic_lookup '
elif v == 'LDCXXSHARED':
return 'g++-4.2 -bundle -undefined dynamic_lookup '
elif v == 'CXX':
return 'g++-4.2'
elif v == 'CC':
return 'gcc-4.2'
return ''
def gcvs(*args, _orig=sysconfig.get_config_vars):
if args:
return list(map(sysconfig.get_config_var, args))
return _orig()
sysconfig.get_config_var = gcv
sysconfig.get_config_vars = gcvs
with mock.patch.object(
self.cc, 'spawn', return_value=None
) as mock_spawn, mock.patch.object(
self.cc, '_need_link', return_value=True
), mock.patch.object(
self.cc, 'mkpath', return_value=None
), EnvironmentVarGuard() as env:
env['CC'] = 'ccache my_cc'
env['CXX'] = 'my_cxx'
del env['LDSHARED']
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so[0:2] == ['ccache', 'my_cc']
self.cc.link(None, [], 'a.out', target_lang='c++')
call_args = mock_spawn.call_args[0][0]
expected = ['my_cxx', '-bundle', '-undefined', 'dynamic_lookup']
assert call_args[:4] == expected
@pytest.mark.skipif('platform.system == "Windows"')
def test_explicit_ldshared(self):
# Issue #18080:
# ensure that setting CC env variable does not change
# explicit LDSHARED setting for linker
def gcv(v):
if v == 'LDSHARED':
return 'gcc-4.2 -bundle -undefined dynamic_lookup '
return 'gcc-4.2'
def gcvs(*args, _orig=sysconfig.get_config_vars):
if args:
return list(map(sysconfig.get_config_var, args))
return _orig()
sysconfig.get_config_var = gcv
sysconfig.get_config_vars = gcvs
with EnvironmentVarGuard() as env:
env['CC'] = 'my_cc'
env['LDSHARED'] = 'my_ld -bundle -dynamic'
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so[0] == 'my_ld'
def test_has_function(self):
# Issue https://github.com/pypa/distutils/issues/64:
# ensure that setting output_dir does not raise
# FileNotFoundError: [Errno 2] No such file or directory: 'a.out'
self.cc.output_dir = 'scratch'
os.chdir(self.mkdtemp())
self.cc.has_function('abort')
def test_find_library_file(self, monkeypatch):
compiler = UnixCCompiler()
compiler._library_root = lambda dir: dir
monkeypatch.setattr(os.path, 'exists', lambda d: 'existing' in d)
libname = 'libabc.dylib' if sys.platform != 'cygwin' else 'cygabc.dll'
dirs = ('/foo/bar/missing', '/foo/bar/existing')
assert (
compiler.find_library_file(dirs, 'abc').replace('\\', '/')
== f'/foo/bar/existing/{libname}'
)
assert (
compiler.find_library_file(reversed(dirs), 'abc').replace('\\', '/')
== f'/foo/bar/existing/{libname}'
)
monkeypatch.setattr(
os.path,
'exists',
lambda d: 'existing' in d and '.a' in d and '.dll.a' not in d,
)
assert (
compiler.find_library_file(dirs, 'abc').replace('\\', '/')
== '/foo/bar/existing/libabc.a'
)
assert (
compiler.find_library_file(reversed(dirs), 'abc').replace('\\', '/')
== '/foo/bar/existing/libabc.a'
)

Sorry, the diff of this file is not supported yet

Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Metadata-Version: 2.1
Name: importlib_resources
Version: 6.4.0
Summary: Read resources from Python packages
Home-page: https://github.com/python/importlib_resources
Author: Barry Warsaw
Author-email: barry@python.org
Project-URL: Documentation, https://importlib-resources.readthedocs.io/
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.8
License-File: LICENSE
Requires-Dist: zipp >=3.1.0 ; python_version < "3.10"
Provides-Extra: docs
Requires-Dist: sphinx >=3.5 ; extra == 'docs'
Requires-Dist: sphinx <7.2.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'
Requires-Dist: jaraco.tidelift >=1.4 ; extra == 'docs'
Provides-Extra: testing
Requires-Dist: pytest >=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: pytest-ruff >=0.2.1 ; extra == 'testing'
Requires-Dist: zipp >=3.17 ; extra == 'testing'
Requires-Dist: jaraco.test >=5.4 ; extra == 'testing'
Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing'
.. image:: https://img.shields.io/pypi/v/importlib_resources.svg
:target: https://pypi.org/project/importlib_resources
.. image:: https://img.shields.io/pypi/pyversions/importlib_resources.svg
.. image:: https://github.com/python/importlib_resources/actions/workflows/main.yml/badge.svg
:target: https://github.com/python/importlib_resources/actions?query=workflow%3A%22tests%22
:alt: tests
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
:target: https://github.com/astral-sh/ruff
:alt: Ruff
.. image:: https://readthedocs.org/projects/importlib-resources/badge/?version=latest
:target: https://importlib-resources.readthedocs.io/en/latest/?badge=latest
.. image:: https://img.shields.io/badge/skeleton-2024-informational
:target: https://blog.jaraco.com/skeleton
.. image:: https://tidelift.com/badges/package/pypi/importlib-resources
:target: https://tidelift.com/subscription/pkg/pypi-importlib-resources?utm_source=pypi-importlib-resources&utm_medium=readme
``importlib_resources`` is a backport of Python standard library
`importlib.resources
<https://docs.python.org/3/library/importlib.html#module-importlib.resources>`_
module for older Pythons.
The key goal of this module is to replace parts of `pkg_resources
<https://setuptools.readthedocs.io/en/latest/pkg_resources.html>`_ with a
solution in Python's stdlib that relies on well-defined APIs. This makes
reading resources included in packages easier, with more stable and consistent
semantics.
Compatibility
=============
New features are introduced in this third-party library and later merged
into CPython. The following table indicates which versions of this library
were contributed to different versions in the standard library:
.. list-table::
:header-rows: 1
* - importlib_resources
- stdlib
* - 6.0
- 3.13
* - 5.12
- 3.12
* - 5.7
- 3.11
* - 5.0
- 3.10
* - 1.3
- 3.9
* - 0.5 (?)
- 3.7
For Enterprise
==============
Available as part of the Tidelift Subscription.
This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
`Learn more <https://tidelift.com/subscription/pkg/pypi-importlib-resources?utm_source=pypi-importlib-resources&utm_medium=referral&utm_campaign=github>`_.
importlib_resources-6.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
importlib_resources-6.4.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
importlib_resources-6.4.0.dist-info/METADATA,sha256=g4eM2LuL0OiZcUVND0qwDJUpE29gOvtO3BSPXTbO9Fk,3944
importlib_resources-6.4.0.dist-info/RECORD,,
importlib_resources-6.4.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
importlib_resources-6.4.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
importlib_resources-6.4.0.dist-info/top_level.txt,sha256=fHIjHU1GZwAjvcydpmUnUrTnbvdiWjG4OEVZK8by0TQ,20
importlib_resources/__init__.py,sha256=uyp1kzYR6SawQBsqlyaXXfIxJx4Z2mM8MjmZn8qq2Gk,505
importlib_resources/__pycache__/__init__.cpython-312.pyc,,
importlib_resources/__pycache__/_adapters.cpython-312.pyc,,
importlib_resources/__pycache__/_common.cpython-312.pyc,,
importlib_resources/__pycache__/_itertools.cpython-312.pyc,,
importlib_resources/__pycache__/abc.cpython-312.pyc,,
importlib_resources/__pycache__/functional.cpython-312.pyc,,
importlib_resources/__pycache__/readers.cpython-312.pyc,,
importlib_resources/__pycache__/simple.cpython-312.pyc,,
importlib_resources/_adapters.py,sha256=vprJGbUeHbajX6XCuMP6J3lMrqCi-P_MTlziJUR7jfk,4482
importlib_resources/_common.py,sha256=blt4-ZtHnbUPzQQyPP7jLGgl_86btIW5ZhIsEhclhoA,5571
importlib_resources/_itertools.py,sha256=eDisV6RqiNZOogLSXf6LOGHOYc79FGgPrKNLzFLmCrU,1277
importlib_resources/abc.py,sha256=UKNU9ncEDkZRB3txcGb3WLxsL2iju9JbaLTI-dfLE_4,5162
importlib_resources/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
importlib_resources/compat/__pycache__/__init__.cpython-312.pyc,,
importlib_resources/compat/__pycache__/py38.cpython-312.pyc,,
importlib_resources/compat/__pycache__/py39.cpython-312.pyc,,
importlib_resources/compat/py38.py,sha256=MWhut3XsAJwBYUaa5Qb2AoCrZNqcQjVThP-P1uBoE_4,230
importlib_resources/compat/py39.py,sha256=Wfln4uQUShNz1XdCG-toG6_Y0WrlUmO9JzpvtcfQ-Cw,184
importlib_resources/functional.py,sha256=mLU4DwSlh8_2IXWqwKOfPVxyRqAEpB3B4XTfRxr3X3M,2651
importlib_resources/future/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
importlib_resources/future/__pycache__/__init__.cpython-312.pyc,,
importlib_resources/future/__pycache__/adapters.cpython-312.pyc,,
importlib_resources/future/adapters.py,sha256=1-MF2VRcCButhcC1OMfZILU9o3kwZ4nXB2lurXpaIAw,2940
importlib_resources/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
importlib_resources/readers.py,sha256=WNKurBHHVu9EVtUhWkOj2fxH50HP7uanNFuupAqH2S8,5863
importlib_resources/simple.py,sha256=CQ3TiIMFiJs_80o-7xJL1EpbUUVna4-NGDrSTQ3HW2Y,2584
importlib_resources/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
importlib_resources/tests/__pycache__/__init__.cpython-312.pyc,,
importlib_resources/tests/__pycache__/_path.cpython-312.pyc,,
importlib_resources/tests/__pycache__/test_compatibilty_files.cpython-312.pyc,,
importlib_resources/tests/__pycache__/test_contents.cpython-312.pyc,,
importlib_resources/tests/__pycache__/test_custom.cpython-312.pyc,,
importlib_resources/tests/__pycache__/test_files.cpython-312.pyc,,
importlib_resources/tests/__pycache__/test_functional.cpython-312.pyc,,
importlib_resources/tests/__pycache__/test_open.cpython-312.pyc,,
importlib_resources/tests/__pycache__/test_path.cpython-312.pyc,,
importlib_resources/tests/__pycache__/test_read.cpython-312.pyc,,
importlib_resources/tests/__pycache__/test_reader.cpython-312.pyc,,
importlib_resources/tests/__pycache__/test_resource.cpython-312.pyc,,
importlib_resources/tests/__pycache__/util.cpython-312.pyc,,
importlib_resources/tests/__pycache__/zip.cpython-312.pyc,,
importlib_resources/tests/_path.py,sha256=nkv3ek7D1U898v921rYbldDCtKri2oyYOi3EJqGjEGU,1289
importlib_resources/tests/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
importlib_resources/tests/compat/__pycache__/__init__.cpython-312.pyc,,
importlib_resources/tests/compat/__pycache__/py312.cpython-312.pyc,,
importlib_resources/tests/compat/__pycache__/py39.cpython-312.pyc,,
importlib_resources/tests/compat/py312.py,sha256=qcWjpZhQo2oEsdwIlRRQHrsMGDltkFTnETeG7fLdUS8,364
importlib_resources/tests/compat/py39.py,sha256=lRTk0RWAOEb9RzAgvdRnqJUGCBLc3qoFQwzuJSa_zP4,329
importlib_resources/tests/data01/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
importlib_resources/tests/data01/__pycache__/__init__.cpython-312.pyc,,
importlib_resources/tests/data01/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4
importlib_resources/tests/data01/subdirectory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
importlib_resources/tests/data01/subdirectory/__pycache__/__init__.cpython-312.pyc,,
importlib_resources/tests/data01/subdirectory/binary.file,sha256=xtRM9Bj2EOP-nh2SlP9D3vgcbNytbLsYIM_0jTqkNV0,4
importlib_resources/tests/data01/utf-16.file,sha256=t5q9qhxX0rYqItBOM8D3ylwG-RHrnOYteTLtQr6sF7g,44
importlib_resources/tests/data01/utf-8.file,sha256=kwWgYG4yQ-ZF2X_WA66EjYPmxJRn-w8aSOiS9e8tKYY,20
importlib_resources/tests/data02/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
importlib_resources/tests/data02/__pycache__/__init__.cpython-312.pyc,,
importlib_resources/tests/data02/one/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
importlib_resources/tests/data02/one/__pycache__/__init__.cpython-312.pyc,,
importlib_resources/tests/data02/one/resource1.txt,sha256=10flKac7c-XXFzJ3t-AB5MJjlBy__dSZvPE_dOm2q6U,13
importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt,sha256=jnrBBztxYrtQck7cmVnc4xQVO4-agzAZDGSFkAWtlFw,10
importlib_resources/tests/data02/two/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
importlib_resources/tests/data02/two/__pycache__/__init__.cpython-312.pyc,,
importlib_resources/tests/data02/two/resource2.txt,sha256=lt2jbN3TMn9QiFKM832X39bU_62UptDdUkoYzkvEbl0,13
importlib_resources/tests/namespacedata01/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4
importlib_resources/tests/namespacedata01/subdirectory/binary.file,sha256=cbkhEL8TXIVYHIoSj2oZwPasp1KwxskeNXGJnPCbFF0,4
importlib_resources/tests/namespacedata01/utf-16.file,sha256=t5q9qhxX0rYqItBOM8D3ylwG-RHrnOYteTLtQr6sF7g,44
importlib_resources/tests/namespacedata01/utf-8.file,sha256=kwWgYG4yQ-ZF2X_WA66EjYPmxJRn-w8aSOiS9e8tKYY,20
importlib_resources/tests/test_compatibilty_files.py,sha256=95N_R7aik8cvnE6sBJpsxmP0K5plOWRIJDgbalD-Hpw,3314
importlib_resources/tests/test_contents.py,sha256=70HW3mL_hv05Emv-OgdmwoLhXxjtuVxiWVaUpgRaRWA,930
importlib_resources/tests/test_custom.py,sha256=QrHZqIWl0e-fsQRfm0ych8stOlKJOsAIU3rK6QOcyN0,1221
importlib_resources/tests/test_files.py,sha256=OcShYu33kCcyXlDyZSVPkJNE08h-N_4bQOLV2QaSqX0,3472
importlib_resources/tests/test_functional.py,sha256=ByCVViAwb2PIlKvDNJEqTZ0aLZGpFl5qa7CMCX-7HKM,8591
importlib_resources/tests/test_open.py,sha256=ccmzbOeEa6zTd4ymZZ8yISrecfuYV0jhon-Vddqysu4,2778
importlib_resources/tests/test_path.py,sha256=x8r2gJxG3hFM9xCOFNkgmHYXxsMldMLTSW_AZYf1l-A,2009
importlib_resources/tests/test_read.py,sha256=7tsILQ2NoqVGFQxhHqxBwc5hWcN8b_3idojCsszTNfQ,3112
importlib_resources/tests/test_reader.py,sha256=IcIUXaiPAtuahGV4_ZT4YXFLMMsJmcM1iOxqdIH2Aa4,5001
importlib_resources/tests/test_resource.py,sha256=fcF8WgZ6rDCTRFnxtAUbdiaNe4G23yGovT1nb2dc7ls,7823
importlib_resources/tests/util.py,sha256=vjVzEyX0X2RkTN-wGiQiplayp9sZom4JDjJinTNewos,4745
importlib_resources/tests/zip.py,sha256=2MKmF8-osXBJSnqcUTuAUek_-tSB3iKmIT9qPhcsOsM,783

Sorry, the diff of this file is not supported yet

Wheel-Version: 1.0
Generator: bdist_wheel (0.43.0)
Root-Is-Purelib: true
Tag: py3-none-any
"""Read resources contained within a package."""
from ._common import (
as_file,
files,
Package,
Anchor,
)
from .functional import (
contents,
is_resource,
open_binary,
open_text,
path,
read_binary,
read_text,
)
from .abc import ResourceReader
__all__ = [
'Package',
'Anchor',
'ResourceReader',
'as_file',
'files',
'contents',
'is_resource',
'open_binary',
'open_text',
'path',
'read_binary',
'read_text',
]
from contextlib import suppress
from io import TextIOWrapper
from . import abc
class SpecLoaderAdapter:
"""
Adapt a package spec to adapt the underlying loader.
"""
def __init__(self, spec, adapter=lambda spec: spec.loader):
self.spec = spec
self.loader = adapter(spec)
def __getattr__(self, name):
return getattr(self.spec, name)
class TraversableResourcesLoader:
"""
Adapt a loader to provide TraversableResources.
"""
def __init__(self, spec):
self.spec = spec
def get_resource_reader(self, name):
return CompatibilityFiles(self.spec)._native()
def _io_wrapper(file, mode='r', *args, **kwargs):
if mode == 'r':
return TextIOWrapper(file, *args, **kwargs)
elif mode == 'rb':
return file
raise ValueError(f"Invalid mode value '{mode}', only 'r' and 'rb' are supported")
class CompatibilityFiles:
"""
Adapter for an existing or non-existent resource reader
to provide a compatibility .files().
"""
class SpecPath(abc.Traversable):
"""
Path tied to a module spec.
Can be read and exposes the resource reader children.
"""
def __init__(self, spec, reader):
self._spec = spec
self._reader = reader
def iterdir(self):
if not self._reader:
return iter(())
return iter(
CompatibilityFiles.ChildPath(self._reader, path)
for path in self._reader.contents()
)
def is_file(self):
return False
is_dir = is_file
def joinpath(self, other):
if not self._reader:
return CompatibilityFiles.OrphanPath(other)
return CompatibilityFiles.ChildPath(self._reader, other)
@property
def name(self):
return self._spec.name
def open(self, mode='r', *args, **kwargs):
return _io_wrapper(self._reader.open_resource(None), mode, *args, **kwargs)
class ChildPath(abc.Traversable):
"""
Path tied to a resource reader child.
Can be read but doesn't expose any meaningful children.
"""
def __init__(self, reader, name):
self._reader = reader
self._name = name
def iterdir(self):
return iter(())
def is_file(self):
return self._reader.is_resource(self.name)
def is_dir(self):
return not self.is_file()
def joinpath(self, other):
return CompatibilityFiles.OrphanPath(self.name, other)
@property
def name(self):
return self._name
def open(self, mode='r', *args, **kwargs):
return _io_wrapper(
self._reader.open_resource(self.name), mode, *args, **kwargs
)
class OrphanPath(abc.Traversable):
"""
Orphan path, not tied to a module spec or resource reader.
Can't be read and doesn't expose any meaningful children.
"""
def __init__(self, *path_parts):
if len(path_parts) < 1:
raise ValueError('Need at least one path part to construct a path')
self._path = path_parts
def iterdir(self):
return iter(())
def is_file(self):
return False
is_dir = is_file
def joinpath(self, other):
return CompatibilityFiles.OrphanPath(*self._path, other)
@property
def name(self):
return self._path[-1]
def open(self, mode='r', *args, **kwargs):
raise FileNotFoundError("Can't open orphan path")
def __init__(self, spec):
self.spec = spec
@property
def _reader(self):
with suppress(AttributeError):
return self.spec.loader.get_resource_reader(self.spec.name)
def _native(self):
"""
Return the native reader if it supports files().
"""
reader = self._reader
return reader if hasattr(reader, 'files') else self
def __getattr__(self, attr):
return getattr(self._reader, attr)
def files(self):
return CompatibilityFiles.SpecPath(self.spec, self._reader)
def wrap_spec(package):
"""
Construct a package spec with traversable compatibility
on the spec/loader/reader.
"""
return SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader)
import os
import pathlib
import tempfile
import functools
import contextlib
import types
import importlib
import inspect
import warnings
import itertools
from typing import Union, Optional, cast
from .abc import ResourceReader, Traversable
Package = Union[types.ModuleType, str]
Anchor = Package
def package_to_anchor(func):
"""
Replace 'package' parameter as 'anchor' and warn about the change.
Other errors should fall through.
>>> files('a', 'b')
Traceback (most recent call last):
TypeError: files() takes from 0 to 1 positional arguments but 2 were given
Remove this compatibility in Python 3.14.
"""
undefined = object()
@functools.wraps(func)
def wrapper(anchor=undefined, package=undefined):
if package is not undefined:
if anchor is not undefined:
return func(anchor, package)
warnings.warn(
"First parameter to files is renamed to 'anchor'",
DeprecationWarning,
stacklevel=2,
)
return func(package)
elif anchor is undefined:
return func()
return func(anchor)
return wrapper
@package_to_anchor
def files(anchor: Optional[Anchor] = None) -> Traversable:
"""
Get a Traversable resource for an anchor.
"""
return from_package(resolve(anchor))
def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:
"""
Return the package's loader if it's a ResourceReader.
"""
# We can't use
# a issubclass() check here because apparently abc.'s __subclasscheck__()
# hook wants to create a weak reference to the object, but
# zipimport.zipimporter does not support weak references, resulting in a
# TypeError. That seems terrible.
spec = package.__spec__
reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore
if reader is None:
return None
return reader(spec.name) # type: ignore
@functools.singledispatch
def resolve(cand: Optional[Anchor]) -> types.ModuleType:
return cast(types.ModuleType, cand)
@resolve.register
def _(cand: str) -> types.ModuleType:
return importlib.import_module(cand)
@resolve.register
def _(cand: None) -> types.ModuleType:
return resolve(_infer_caller().f_globals['__name__'])
def _infer_caller():
"""
Walk the stack and find the frame of the first caller not in this module.
"""
def is_this_file(frame_info):
return frame_info.filename == __file__
def is_wrapper(frame_info):
return frame_info.function == 'wrapper'
not_this_file = itertools.filterfalse(is_this_file, inspect.stack())
# also exclude 'wrapper' due to singledispatch in the call stack
callers = itertools.filterfalse(is_wrapper, not_this_file)
return next(callers).frame
def from_package(package: types.ModuleType):
"""
Return a Traversable object for the given package.
"""
# deferred for performance (python/cpython#109829)
from .future.adapters import wrap_spec
spec = wrap_spec(package)
reader = spec.loader.get_resource_reader(spec.name)
return reader.files()
@contextlib.contextmanager
def _tempfile(
reader,
suffix='',
# gh-93353: Keep a reference to call os.remove() in late Python
# finalization.
*,
_os_remove=os.remove,
):
# Not using tempfile.NamedTemporaryFile as it leads to deeper 'try'
# blocks due to the need to close the temporary file to work on Windows
# properly.
fd, raw_path = tempfile.mkstemp(suffix=suffix)
try:
try:
os.write(fd, reader())
finally:
os.close(fd)
del reader
yield pathlib.Path(raw_path)
finally:
try:
_os_remove(raw_path)
except FileNotFoundError:
pass
def _temp_file(path):
return _tempfile(path.read_bytes, suffix=path.name)
def _is_present_dir(path: Traversable) -> bool:
"""
Some Traversables implement ``is_dir()`` to raise an
exception (i.e. ``FileNotFoundError``) when the
directory doesn't exist. This function wraps that call
to always return a boolean and only return True
if there's a dir and it exists.
"""
with contextlib.suppress(FileNotFoundError):
return path.is_dir()
return False
@functools.singledispatch
def as_file(path):
"""
Given a Traversable object, return that object as a
path on the local file system in a context manager.
"""
return _temp_dir(path) if _is_present_dir(path) else _temp_file(path)
@as_file.register(pathlib.Path)
@contextlib.contextmanager
def _(path):
"""
Degenerate behavior for pathlib.Path objects.
"""
yield path
@contextlib.contextmanager
def _temp_path(dir: tempfile.TemporaryDirectory):
"""
Wrap tempfile.TemporyDirectory to return a pathlib object.
"""
with dir as result:
yield pathlib.Path(result)
@contextlib.contextmanager
def _temp_dir(path):
"""
Given a traversable dir, recursively replicate the whole tree
to the file system in a context manager.
"""
assert path.is_dir()
with _temp_path(tempfile.TemporaryDirectory()) as temp_dir:
yield _write_contents(temp_dir, path)
def _write_contents(target, source):
child = target.joinpath(source.name)
if source.is_dir():
child.mkdir()
for item in source.iterdir():
_write_contents(child, item)
else:
child.write_bytes(source.read_bytes())
return child
# from more_itertools 9.0
def only(iterable, default=None, too_long=None):
"""If *iterable* has only one item, return it.
If it has zero items, return *default*.
If it has more than one item, raise the exception given by *too_long*,
which is ``ValueError`` by default.
>>> only([], default='missing')
'missing'
>>> only([1])
1
>>> only([1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValueError: Expected exactly one item in iterable, but got 1, 2,
and perhaps more.'
>>> only([1, 2], too_long=TypeError) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError
Note that :func:`only` attempts to advance *iterable* twice to ensure there
is only one item. See :func:`spy` or :func:`peekable` to check
iterable contents less destructively.
"""
it = iter(iterable)
first_value = next(it, default)
try:
second_value = next(it)
except StopIteration:
pass
else:
msg = (
'Expected exactly one item in iterable, but got {!r}, {!r}, '
'and perhaps more.'.format(first_value, second_value)
)
raise too_long or ValueError(msg)
return first_value
import abc
import io
import itertools
import pathlib
from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional
from typing import runtime_checkable, Protocol
from .compat.py38 import StrPath
__all__ = ["ResourceReader", "Traversable", "TraversableResources"]
class ResourceReader(metaclass=abc.ABCMeta):
"""Abstract base class for loaders to provide resource reading support."""
@abc.abstractmethod
def open_resource(self, resource: Text) -> BinaryIO:
"""Return an opened, file-like object for binary reading.
The 'resource' argument is expected to represent only a file name.
If the resource cannot be found, FileNotFoundError is raised.
"""
# This deliberately raises FileNotFoundError instead of
# NotImplementedError so that if this method is accidentally called,
# it'll still do the right thing.
raise FileNotFoundError
@abc.abstractmethod
def resource_path(self, resource: Text) -> Text:
"""Return the file system path to the specified resource.
The 'resource' argument is expected to represent only a file name.
If the resource does not exist on the file system, raise
FileNotFoundError.
"""
# This deliberately raises FileNotFoundError instead of
# NotImplementedError so that if this method is accidentally called,
# it'll still do the right thing.
raise FileNotFoundError
@abc.abstractmethod
def is_resource(self, path: Text) -> bool:
"""Return True if the named 'path' is a resource.
Files are resources, directories are not.
"""
raise FileNotFoundError
@abc.abstractmethod
def contents(self) -> Iterable[str]:
"""Return an iterable of entries in `package`."""
raise FileNotFoundError
class TraversalError(Exception):
pass
@runtime_checkable
class Traversable(Protocol):
"""
An object with a subset of pathlib.Path methods suitable for
traversing directories and opening files.
Any exceptions that occur when accessing the backing resource
may propagate unaltered.
"""
@abc.abstractmethod
def iterdir(self) -> Iterator["Traversable"]:
"""
Yield Traversable objects in self
"""
def read_bytes(self) -> bytes:
"""
Read contents of self as bytes
"""
with self.open('rb') as strm:
return strm.read()
def read_text(self, encoding: Optional[str] = None) -> str:
"""
Read contents of self as text
"""
with self.open(encoding=encoding) as strm:
return strm.read()
@abc.abstractmethod
def is_dir(self) -> bool:
"""
Return True if self is a directory
"""
@abc.abstractmethod
def is_file(self) -> bool:
"""
Return True if self is a file
"""
def joinpath(self, *descendants: StrPath) -> "Traversable":
"""
Return Traversable resolved with any descendants applied.
Each descendant should be a path segment relative to self
and each may contain multiple levels separated by
``posixpath.sep`` (``/``).
"""
if not descendants:
return self
names = itertools.chain.from_iterable(
path.parts for path in map(pathlib.PurePosixPath, descendants)
)
target = next(names)
matches = (
traversable for traversable in self.iterdir() if traversable.name == target
)
try:
match = next(matches)
except StopIteration:
raise TraversalError(
"Target not found during traversal.", target, list(names)
)
return match.joinpath(*names)
def __truediv__(self, child: StrPath) -> "Traversable":
"""
Return Traversable child in self
"""
return self.joinpath(child)
@abc.abstractmethod
def open(self, mode='r', *args, **kwargs):
"""
mode may be 'r' or 'rb' to open as text or binary. Return a handle
suitable for reading (same as pathlib.Path.open).
When opening as text, accepts encoding parameters such as those
accepted by io.TextIOWrapper.
"""
@property
@abc.abstractmethod
def name(self) -> str:
"""
The base name of this object without any parent references.
"""
class TraversableResources(ResourceReader):
"""
The required interface for providing traversable
resources.
"""
@abc.abstractmethod
def files(self) -> "Traversable":
"""Return a Traversable object for the loaded package."""
def open_resource(self, resource: StrPath) -> io.BufferedReader:
return self.files().joinpath(resource).open('rb')
def resource_path(self, resource: Any) -> NoReturn:
raise FileNotFoundError(resource)
def is_resource(self, path: StrPath) -> bool:
return self.files().joinpath(path).is_file()
def contents(self) -> Iterator[str]:
return (item.name for item in self.files().iterdir())
import os
import sys
from typing import Union
if sys.version_info >= (3, 9):
StrPath = Union[str, os.PathLike[str]]
else:
# PathLike is only subscriptable at runtime in 3.9+
StrPath = Union[str, "os.PathLike[str]"]
import sys
__all__ = ['ZipPath']
if sys.version_info >= (3, 10):
from zipfile import Path as ZipPath # type: ignore
else:
from zipp import Path as ZipPath # type: ignore
"""Simplified function-based API for importlib.resources"""
import warnings
from ._common import files, as_file
_MISSING = object()
def open_binary(anchor, *path_names):
"""Open for binary reading the *resource* within *package*."""
return _get_resource(anchor, path_names).open('rb')
def open_text(anchor, *path_names, encoding=_MISSING, errors='strict'):
"""Open for text reading the *resource* within *package*."""
encoding = _get_encoding_arg(path_names, encoding)
resource = _get_resource(anchor, path_names)
return resource.open('r', encoding=encoding, errors=errors)
def read_binary(anchor, *path_names):
"""Read and return contents of *resource* within *package* as bytes."""
return _get_resource(anchor, path_names).read_bytes()
def read_text(anchor, *path_names, encoding=_MISSING, errors='strict'):
"""Read and return contents of *resource* within *package* as str."""
encoding = _get_encoding_arg(path_names, encoding)
resource = _get_resource(anchor, path_names)
return resource.read_text(encoding=encoding, errors=errors)
def path(anchor, *path_names):
"""Return the path to the *resource* as an actual file system path."""
return as_file(_get_resource(anchor, path_names))
def is_resource(anchor, *path_names):
"""Return ``True`` if there is a resource named *name* in the package,
Otherwise returns ``False``.
"""
return _get_resource(anchor, path_names).is_file()
def contents(anchor, *path_names):
"""Return an iterable over the named resources within the package.
The iterable returns :class:`str` resources (e.g. files).
The iterable does not recurse into subdirectories.
"""
warnings.warn(
"importlib.resources.contents is deprecated. "
"Use files(anchor).iterdir() instead.",
DeprecationWarning,
stacklevel=1,
)
return (resource.name for resource in _get_resource(anchor, path_names).iterdir())
def _get_encoding_arg(path_names, encoding):
# For compatibility with versions where *encoding* was a positional
# argument, it needs to be given explicitly when there are multiple
# *path_names*.
# This limitation can be removed in Python 3.15.
if encoding is _MISSING:
if len(path_names) > 1:
raise TypeError(
"'encoding' argument required with multiple path names",
)
else:
return 'utf-8'
return encoding
def _get_resource(anchor, path_names):
if anchor is None:
raise TypeError("anchor must be module or string, got None")
return files(anchor).joinpath(*path_names)
import functools
import pathlib
from contextlib import suppress
from types import SimpleNamespace
from .. import readers, _adapters
def _block_standard(reader_getter):
"""
Wrap _adapters.TraversableResourcesLoader.get_resource_reader
and intercept any standard library readers.
"""
@functools.wraps(reader_getter)
def wrapper(*args, **kwargs):
"""
If the reader is from the standard library, return None to allow
allow likely newer implementations in this library to take precedence.
"""
try:
reader = reader_getter(*args, **kwargs)
except NotADirectoryError:
# MultiplexedPath may fail on zip subdirectory
return
# Python 3.10+
mod_name = reader.__class__.__module__
if mod_name.startswith('importlib.') and mod_name.endswith('readers'):
return
# Python 3.8, 3.9
if isinstance(reader, _adapters.CompatibilityFiles) and (
reader.spec.loader.__class__.__module__.startswith('zipimport')
or reader.spec.loader.__class__.__module__.startswith(
'_frozen_importlib_external'
)
):
return
return reader
return wrapper
def _skip_degenerate(reader):
"""
Mask any degenerate reader. Ref #298.
"""
is_degenerate = (
isinstance(reader, _adapters.CompatibilityFiles) and not reader._reader
)
return reader if not is_degenerate else None
class TraversableResourcesLoader(_adapters.TraversableResourcesLoader):
"""
Adapt loaders to provide TraversableResources and other
compatibility.
Ensures the readers from importlib_resources are preferred
over stdlib readers.
"""
def get_resource_reader(self, name):
return (
_skip_degenerate(_block_standard(super().get_resource_reader)(name))
or self._standard_reader()
or super().get_resource_reader(name)
)
def _standard_reader(self):
return self._zip_reader() or self._namespace_reader() or self._file_reader()
def _zip_reader(self):
with suppress(AttributeError):
return readers.ZipReader(self.spec.loader, self.spec.name)
def _namespace_reader(self):
with suppress(AttributeError, ValueError):
return readers.NamespaceReader(self.spec.submodule_search_locations)
def _file_reader(self):
try:
path = pathlib.Path(self.spec.origin)
except TypeError:
return None
if path.exists():
return readers.FileReader(SimpleNamespace(path=path))
def wrap_spec(package):
"""
Override _adapters.wrap_spec to use TraversableResourcesLoader
from above. Ensures that future behavior is always available on older
Pythons.
"""
return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader)
import collections
import contextlib
import itertools
import pathlib
import operator
import re
import warnings
from . import abc
from ._itertools import only
from .compat.py39 import ZipPath
def remove_duplicates(items):
return iter(collections.OrderedDict.fromkeys(items))
class FileReader(abc.TraversableResources):
def __init__(self, loader):
self.path = pathlib.Path(loader.path).parent
def resource_path(self, resource):
"""
Return the file system path to prevent
`resources.path()` from creating a temporary
copy.
"""
return str(self.path.joinpath(resource))
def files(self):
return self.path
class ZipReader(abc.TraversableResources):
def __init__(self, loader, module):
_, _, name = module.rpartition('.')
self.prefix = loader.prefix.replace('\\', '/') + name + '/'
self.archive = loader.archive
def open_resource(self, resource):
try:
return super().open_resource(resource)
except KeyError as exc:
raise FileNotFoundError(exc.args[0])
def is_resource(self, path):
"""
Workaround for `zipfile.Path.is_file` returning true
for non-existent paths.
"""
target = self.files().joinpath(path)
return target.is_file() and target.exists()
def files(self):
return ZipPath(self.archive, self.prefix)
class MultiplexedPath(abc.Traversable):
"""
Given a series of Traversable objects, implement a merged
version of the interface across all objects. Useful for
namespace packages which may be multihomed at a single
name.
"""
def __init__(self, *paths):
self._paths = list(map(_ensure_traversable, remove_duplicates(paths)))
if not self._paths:
message = 'MultiplexedPath must contain at least one path'
raise FileNotFoundError(message)
if not all(path.is_dir() for path in self._paths):
raise NotADirectoryError('MultiplexedPath only supports directories')
def iterdir(self):
children = (child for path in self._paths for child in path.iterdir())
by_name = operator.attrgetter('name')
groups = itertools.groupby(sorted(children, key=by_name), key=by_name)
return map(self._follow, (locs for name, locs in groups))
def read_bytes(self):
raise FileNotFoundError(f'{self} is not a file')
def read_text(self, *args, **kwargs):
raise FileNotFoundError(f'{self} is not a file')
def is_dir(self):
return True
def is_file(self):
return False
def joinpath(self, *descendants):
try:
return super().joinpath(*descendants)
except abc.TraversalError:
# One of the paths did not resolve (a directory does not exist).
# Just return something that will not exist.
return self._paths[0].joinpath(*descendants)
@classmethod
def _follow(cls, children):
"""
Construct a MultiplexedPath if needed.
If children contains a sole element, return it.
Otherwise, return a MultiplexedPath of the items.
Unless one of the items is not a Directory, then return the first.
"""
subdirs, one_dir, one_file = itertools.tee(children, 3)
try:
return only(one_dir)
except ValueError:
try:
return cls(*subdirs)
except NotADirectoryError:
return next(one_file)
def open(self, *args, **kwargs):
raise FileNotFoundError(f'{self} is not a file')
@property
def name(self):
return self._paths[0].name
def __repr__(self):
paths = ', '.join(f"'{path}'" for path in self._paths)
return f'MultiplexedPath({paths})'
class NamespaceReader(abc.TraversableResources):
def __init__(self, namespace_path):
if 'NamespacePath' not in str(namespace_path):
raise ValueError('Invalid path')
self.path = MultiplexedPath(*map(self._resolve, namespace_path))
@classmethod
def _resolve(cls, path_str) -> abc.Traversable:
r"""
Given an item from a namespace path, resolve it to a Traversable.
path_str might be a directory on the filesystem or a path to a
zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or
``/foo/baz.zip/inner_dir`` or ``foo\baz.zip\inner_dir\sub``.
"""
(dir,) = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir())
return dir
@classmethod
def _candidate_paths(cls, path_str):
yield pathlib.Path(path_str)
yield from cls._resolve_zip_path(path_str)
@staticmethod
def _resolve_zip_path(path_str):
for match in reversed(list(re.finditer(r'[\\/]', path_str))):
with contextlib.suppress(
FileNotFoundError,
IsADirectoryError,
NotADirectoryError,
PermissionError,
):
inner = path_str[match.end() :].replace('\\', '/') + '/'
yield ZipPath(path_str[: match.start()], inner.lstrip('/'))
def resource_path(self, resource):
"""
Return the file system path to prevent
`resources.path()` from creating a temporary
copy.
"""
return str(self.path.joinpath(resource))
def files(self):
return self.path
def _ensure_traversable(path):
"""
Convert deprecated string arguments to traversables (pathlib.Path).
Remove with Python 3.15.
"""
if not isinstance(path, str):
return path
warnings.warn(
"String arguments are deprecated. Pass a Traversable instead.",
DeprecationWarning,
stacklevel=3,
)
return pathlib.Path(path)
"""
Interface adapters for low-level readers.
"""
import abc
import io
import itertools
from typing import BinaryIO, List
from .abc import Traversable, TraversableResources
class SimpleReader(abc.ABC):
"""
The minimum, low-level interface required from a resource
provider.
"""
@property
@abc.abstractmethod
def package(self) -> str:
"""
The name of the package for which this reader loads resources.
"""
@abc.abstractmethod
def children(self) -> List['SimpleReader']:
"""
Obtain an iterable of SimpleReader for available
child containers (e.g. directories).
"""
@abc.abstractmethod
def resources(self) -> List[str]:
"""
Obtain available named resources for this virtual package.
"""
@abc.abstractmethod
def open_binary(self, resource: str) -> BinaryIO:
"""
Obtain a File-like for a named resource.
"""
@property
def name(self):
return self.package.split('.')[-1]
class ResourceContainer(Traversable):
"""
Traversable container for a package's resources via its reader.
"""
def __init__(self, reader: SimpleReader):
self.reader = reader
def is_dir(self):
return True
def is_file(self):
return False
def iterdir(self):
files = (ResourceHandle(self, name) for name in self.reader.resources)
dirs = map(ResourceContainer, self.reader.children())
return itertools.chain(files, dirs)
def open(self, *args, **kwargs):
raise IsADirectoryError()
class ResourceHandle(Traversable):
"""
Handle to a named resource in a ResourceReader.
"""
def __init__(self, parent: ResourceContainer, name: str):
self.parent = parent
self.name = name # type: ignore
def is_file(self):
return True
def is_dir(self):
return False
def open(self, mode='r', *args, **kwargs):
stream = self.parent.reader.open_binary(self.name)
if 'b' not in mode:
stream = io.TextIOWrapper(stream, *args, **kwargs)
return stream
def joinpath(self, name):
raise RuntimeError("Cannot traverse into a resource")
class TraversableReader(TraversableResources, SimpleReader):
"""
A TraversableResources based on SimpleReader. Resource providers
may derive from this class to provide the TraversableResources
interface by supplying the SimpleReader interface.
"""
def files(self):
return ResourceContainer(self)
import pathlib
import functools
from typing import Dict, Union
####
# from jaraco.path 3.4.1
FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore
def build(spec: FilesSpec, prefix=pathlib.Path()):
"""
Build a set of files/directories, as described by the spec.
Each key represents a pathname, and the value represents
the content. Content may be a nested directory.
>>> spec = {
... 'README.txt': "A README file",
... "foo": {
... "__init__.py": "",
... "bar": {
... "__init__.py": "",
... },
... "baz.py": "# Some code",
... }
... }
>>> target = getfixture('tmp_path')
>>> build(spec, target)
>>> target.joinpath('foo/baz.py').read_text(encoding='utf-8')
'# Some code'
"""
for name, contents in spec.items():
create(contents, pathlib.Path(prefix) / name)
@functools.singledispatch
def create(content: Union[str, bytes, FilesSpec], path):
path.mkdir(exist_ok=True)
build(content, prefix=path) # type: ignore
@create.register
def _(content: bytes, path):
path.write_bytes(content)
@create.register
def _(content: str, path):
path.write_text(content, encoding='utf-8')
# end from jaraco.path
####
import contextlib
from .py39 import import_helper
@contextlib.contextmanager
def isolated_modules():
"""
Save modules on entry and cleanup on exit.
"""
(saved,) = import_helper.modules_setup()
try:
yield
finally:
import_helper.modules_cleanup(saved)
vars(import_helper).setdefault('isolated_modules', isolated_modules)
"""
Backward-compatability shims to support Python 3.9 and earlier.
"""
from jaraco.test.cpython import from_test_support, try_import
import_helper = try_import('import_helper') or from_test_support(
'modules_setup', 'modules_cleanup', 'DirsOnSysPath'
)
os_helper = try_import('os_helper') or from_test_support('temp_dir')

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

a resource

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

import io
import unittest
import importlib_resources as resources
from importlib_resources._adapters import (
CompatibilityFiles,
wrap_spec,
)
from . import util
class CompatibilityFilesTests(unittest.TestCase):
@property
def package(self):
bytes_data = io.BytesIO(b'Hello, world!')
return util.create_package(
file=bytes_data,
path='some_path',
contents=('a', 'b', 'c'),
)
@property
def files(self):
return resources.files(self.package)
def test_spec_path_iter(self):
self.assertEqual(
sorted(path.name for path in self.files.iterdir()),
['a', 'b', 'c'],
)
def test_child_path_iter(self):
self.assertEqual(list((self.files / 'a').iterdir()), [])
def test_orphan_path_iter(self):
self.assertEqual(list((self.files / 'a' / 'a').iterdir()), [])
self.assertEqual(list((self.files / 'a' / 'a' / 'a').iterdir()), [])
def test_spec_path_is(self):
self.assertFalse(self.files.is_file())
self.assertFalse(self.files.is_dir())
def test_child_path_is(self):
self.assertTrue((self.files / 'a').is_file())
self.assertFalse((self.files / 'a').is_dir())
def test_orphan_path_is(self):
self.assertFalse((self.files / 'a' / 'a').is_file())
self.assertFalse((self.files / 'a' / 'a').is_dir())
self.assertFalse((self.files / 'a' / 'a' / 'a').is_file())
self.assertFalse((self.files / 'a' / 'a' / 'a').is_dir())
def test_spec_path_name(self):
self.assertEqual(self.files.name, 'testingpackage')
def test_child_path_name(self):
self.assertEqual((self.files / 'a').name, 'a')
def test_orphan_path_name(self):
self.assertEqual((self.files / 'a' / 'b').name, 'b')
self.assertEqual((self.files / 'a' / 'b' / 'c').name, 'c')
def test_spec_path_open(self):
self.assertEqual(self.files.read_bytes(), b'Hello, world!')
self.assertEqual(self.files.read_text(encoding='utf-8'), 'Hello, world!')
def test_child_path_open(self):
self.assertEqual((self.files / 'a').read_bytes(), b'Hello, world!')
self.assertEqual(
(self.files / 'a').read_text(encoding='utf-8'), 'Hello, world!'
)
def test_orphan_path_open(self):
with self.assertRaises(FileNotFoundError):
(self.files / 'a' / 'b').read_bytes()
with self.assertRaises(FileNotFoundError):
(self.files / 'a' / 'b' / 'c').read_bytes()
def test_open_invalid_mode(self):
with self.assertRaises(ValueError):
self.files.open('0')
def test_orphan_path_invalid(self):
with self.assertRaises(ValueError):
CompatibilityFiles.OrphanPath()
def test_wrap_spec(self):
spec = wrap_spec(self.package)
self.assertIsInstance(spec.loader.get_resource_reader(None), CompatibilityFiles)
class CompatibilityFilesNoReaderTests(unittest.TestCase):
@property
def package(self):
return util.create_package_from_loader(None)
@property
def files(self):
return resources.files(self.package)
def test_spec_path_joinpath(self):
self.assertIsInstance(self.files / 'a', CompatibilityFiles.OrphanPath)
import unittest
import importlib_resources as resources
from . import data01
from . import util
class ContentsTests:
expected = {
'__init__.py',
'binary.file',
'subdirectory',
'utf-16.file',
'utf-8.file',
}
def test_contents(self):
contents = {path.name for path in resources.files(self.data).iterdir()}
assert self.expected <= contents
class ContentsDiskTests(ContentsTests, unittest.TestCase):
def setUp(self):
self.data = data01
class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase):
pass
class ContentsNamespaceTests(ContentsTests, unittest.TestCase):
expected = {
# no __init__ because of namespace design
'binary.file',
'subdirectory',
'utf-16.file',
'utf-8.file',
}
def setUp(self):
from . import namespacedata01
self.data = namespacedata01
import unittest
import contextlib
import pathlib
import importlib_resources as resources
from .. import abc
from ..abc import TraversableResources, ResourceReader
from . import util
from .compat.py39 import os_helper
class SimpleLoader:
"""
A simple loader that only implements a resource reader.
"""
def __init__(self, reader: ResourceReader):
self.reader = reader
def get_resource_reader(self, package):
return self.reader
class MagicResources(TraversableResources):
"""
Magically returns the resources at path.
"""
def __init__(self, path: pathlib.Path):
self.path = path
def files(self):
return self.path
class CustomTraversableResourcesTests(unittest.TestCase):
def setUp(self):
self.fixtures = contextlib.ExitStack()
self.addCleanup(self.fixtures.close)
def test_custom_loader(self):
temp_dir = pathlib.Path(self.fixtures.enter_context(os_helper.temp_dir()))
loader = SimpleLoader(MagicResources(temp_dir))
pkg = util.create_package_from_loader(loader)
files = resources.files(pkg)
assert isinstance(files, abc.Traversable)
assert list(files.iterdir()) == []
import textwrap
import unittest
import warnings
import importlib
import contextlib
import importlib_resources as resources
from ..abc import Traversable
from . import data01
from . import util
from . import _path
from .compat.py39 import os_helper
from .compat.py312 import import_helper
@contextlib.contextmanager
def suppress_known_deprecation():
with warnings.catch_warnings(record=True) as ctx:
warnings.simplefilter('default', category=DeprecationWarning)
yield ctx
class FilesTests:
def test_read_bytes(self):
files = resources.files(self.data)
actual = files.joinpath('utf-8.file').read_bytes()
assert actual == b'Hello, UTF-8 world!\n'
def test_read_text(self):
files = resources.files(self.data)
actual = files.joinpath('utf-8.file').read_text(encoding='utf-8')
assert actual == 'Hello, UTF-8 world!\n'
def test_traversable(self):
assert isinstance(resources.files(self.data), Traversable)
def test_joinpath_with_multiple_args(self):
files = resources.files(self.data)
binfile = files.joinpath('subdirectory', 'binary.file')
self.assertTrue(binfile.is_file())
def test_old_parameter(self):
"""
Files used to take a 'package' parameter. Make sure anyone
passing by name is still supported.
"""
with suppress_known_deprecation():
resources.files(package=self.data)
class OpenDiskTests(FilesTests, unittest.TestCase):
def setUp(self):
self.data = data01
class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
pass
class OpenNamespaceTests(FilesTests, unittest.TestCase):
def setUp(self):
from . import namespacedata01
self.data = namespacedata01
class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase):
ZIP_MODULE = 'namespacedata01'
class SiteDir:
def setUp(self):
self.fixtures = contextlib.ExitStack()
self.addCleanup(self.fixtures.close)
self.site_dir = self.fixtures.enter_context(os_helper.temp_dir())
self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir))
self.fixtures.enter_context(import_helper.isolated_modules())
class ModulesFilesTests(SiteDir, unittest.TestCase):
def test_module_resources(self):
"""
A module can have resources found adjacent to the module.
"""
spec = {
'mod.py': '',
'res.txt': 'resources are the best',
}
_path.build(spec, self.site_dir)
import mod
actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8')
assert actual == spec['res.txt']
class ImplicitContextFilesTests(SiteDir, unittest.TestCase):
def test_implicit_files(self):
"""
Without any parameter, files() will infer the location as the caller.
"""
spec = {
'somepkg': {
'__init__.py': textwrap.dedent(
"""
import importlib_resources as res
val = res.files().joinpath('res.txt').read_text(encoding='utf-8')
"""
),
'res.txt': 'resources are the best',
},
}
_path.build(spec, self.site_dir)
assert importlib.import_module('somepkg').val == 'resources are the best'
if __name__ == '__main__':
unittest.main()
import unittest
import os
import contextlib
try:
from test.support.warnings_helper import ignore_warnings, check_warnings
except ImportError:
# older Python versions
from test.support import ignore_warnings, check_warnings
import importlib_resources as resources
# Since the functional API forwards to Traversable, we only test
# filesystem resources here -- not zip files, namespace packages etc.
# We do test for two kinds of Anchor, though.
class StringAnchorMixin:
anchor01 = 'importlib_resources.tests.data01'
anchor02 = 'importlib_resources.tests.data02'
class ModuleAnchorMixin:
from . import data01 as anchor01
from . import data02 as anchor02
class FunctionalAPIBase:
def _gen_resourcetxt_path_parts(self):
"""Yield various names of a text file in anchor02, each in a subTest"""
for path_parts in (
('subdirectory', 'subsubdir', 'resource.txt'),
('subdirectory/subsubdir/resource.txt',),
('subdirectory/subsubdir', 'resource.txt'),
):
with self.subTest(path_parts=path_parts):
yield path_parts
def test_read_text(self):
self.assertEqual(
resources.read_text(self.anchor01, 'utf-8.file'),
'Hello, UTF-8 world!\n',
)
self.assertEqual(
resources.read_text(
self.anchor02,
'subdirectory',
'subsubdir',
'resource.txt',
encoding='utf-8',
),
'a resource',
)
for path_parts in self._gen_resourcetxt_path_parts():
self.assertEqual(
resources.read_text(
self.anchor02,
*path_parts,
encoding='utf-8',
),
'a resource',
)
# Use generic OSError, since e.g. attempting to read a directory can
# fail with PermissionError rather than IsADirectoryError
with self.assertRaises(OSError):
resources.read_text(self.anchor01)
with self.assertRaises(OSError):
resources.read_text(self.anchor01, 'no-such-file')
with self.assertRaises(UnicodeDecodeError):
resources.read_text(self.anchor01, 'utf-16.file')
self.assertEqual(
resources.read_text(
self.anchor01,
'binary.file',
encoding='latin1',
),
'\x00\x01\x02\x03',
)
self.assertEqual(
resources.read_text(
self.anchor01,
'utf-16.file',
errors='backslashreplace',
),
'Hello, UTF-16 world!\n'.encode('utf-16').decode(
errors='backslashreplace',
),
)
def test_read_binary(self):
self.assertEqual(
resources.read_binary(self.anchor01, 'utf-8.file'),
b'Hello, UTF-8 world!\n',
)
for path_parts in self._gen_resourcetxt_path_parts():
self.assertEqual(
resources.read_binary(self.anchor02, *path_parts),
b'a resource',
)
def test_open_text(self):
with resources.open_text(self.anchor01, 'utf-8.file') as f:
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
for path_parts in self._gen_resourcetxt_path_parts():
with resources.open_text(
self.anchor02,
*path_parts,
encoding='utf-8',
) as f:
self.assertEqual(f.read(), 'a resource')
# Use generic OSError, since e.g. attempting to read a directory can
# fail with PermissionError rather than IsADirectoryError
with self.assertRaises(OSError):
resources.open_text(self.anchor01)
with self.assertRaises(OSError):
resources.open_text(self.anchor01, 'no-such-file')
with resources.open_text(self.anchor01, 'utf-16.file') as f:
with self.assertRaises(UnicodeDecodeError):
f.read()
with resources.open_text(
self.anchor01,
'binary.file',
encoding='latin1',
) as f:
self.assertEqual(f.read(), '\x00\x01\x02\x03')
with resources.open_text(
self.anchor01,
'utf-16.file',
errors='backslashreplace',
) as f:
self.assertEqual(
f.read(),
'Hello, UTF-16 world!\n'.encode('utf-16').decode(
errors='backslashreplace',
),
)
def test_open_binary(self):
with resources.open_binary(self.anchor01, 'utf-8.file') as f:
self.assertEqual(f.read(), b'Hello, UTF-8 world!\n')
for path_parts in self._gen_resourcetxt_path_parts():
with resources.open_binary(
self.anchor02,
*path_parts,
) as f:
self.assertEqual(f.read(), b'a resource')
def test_path(self):
with resources.path(self.anchor01, 'utf-8.file') as path:
with open(str(path), encoding='utf-8') as f:
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
with resources.path(self.anchor01) as path:
with open(os.path.join(path, 'utf-8.file'), encoding='utf-8') as f:
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
def test_is_resource(self):
is_resource = resources.is_resource
self.assertTrue(is_resource(self.anchor01, 'utf-8.file'))
self.assertFalse(is_resource(self.anchor01, 'no_such_file'))
self.assertFalse(is_resource(self.anchor01))
self.assertFalse(is_resource(self.anchor01, 'subdirectory'))
for path_parts in self._gen_resourcetxt_path_parts():
self.assertTrue(is_resource(self.anchor02, *path_parts))
def test_contents(self):
with check_warnings((".*contents.*", DeprecationWarning)):
c = resources.contents(self.anchor01)
self.assertGreaterEqual(
set(c),
{'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'},
)
with contextlib.ExitStack() as cm:
cm.enter_context(self.assertRaises(OSError))
cm.enter_context(check_warnings((".*contents.*", DeprecationWarning)))
list(resources.contents(self.anchor01, 'utf-8.file'))
for path_parts in self._gen_resourcetxt_path_parts():
with contextlib.ExitStack() as cm:
cm.enter_context(self.assertRaises(OSError))
cm.enter_context(check_warnings((".*contents.*", DeprecationWarning)))
list(resources.contents(self.anchor01, *path_parts))
with check_warnings((".*contents.*", DeprecationWarning)):
c = resources.contents(self.anchor01, 'subdirectory')
self.assertGreaterEqual(
set(c),
{'binary.file'},
)
@ignore_warnings(category=DeprecationWarning)
def test_common_errors(self):
for func in (
resources.read_text,
resources.read_binary,
resources.open_text,
resources.open_binary,
resources.path,
resources.is_resource,
resources.contents,
):
with self.subTest(func=func):
# Rejecting None anchor
with self.assertRaises(TypeError):
func(None)
# Rejecting invalid anchor type
with self.assertRaises((TypeError, AttributeError)):
func(1234)
# Unknown module
with self.assertRaises(ModuleNotFoundError):
func('$missing module$')
def test_text_errors(self):
for func in (
resources.read_text,
resources.open_text,
):
with self.subTest(func=func):
# Multiple path arguments need explicit encoding argument.
with self.assertRaises(TypeError):
func(
self.anchor02,
'subdirectory',
'subsubdir',
'resource.txt',
)
class FunctionalAPITest_StringAnchor(
unittest.TestCase,
FunctionalAPIBase,
StringAnchorMixin,
):
pass
class FunctionalAPITest_ModuleAnchor(
unittest.TestCase,
FunctionalAPIBase,
ModuleAnchorMixin,
):
pass
import unittest
import importlib_resources as resources
from . import data01
from . import util
class CommonBinaryTests(util.CommonTests, unittest.TestCase):
def execute(self, package, path):
target = resources.files(package).joinpath(path)
with target.open('rb'):
pass
class CommonTextTests(util.CommonTests, unittest.TestCase):
def execute(self, package, path):
target = resources.files(package).joinpath(path)
with target.open(encoding='utf-8'):
pass
class OpenTests:
def test_open_binary(self):
target = resources.files(self.data) / 'binary.file'
with target.open('rb') as fp:
result = fp.read()
self.assertEqual(result, bytes(range(4)))
def test_open_text_default_encoding(self):
target = resources.files(self.data) / 'utf-8.file'
with target.open(encoding='utf-8') as fp:
result = fp.read()
self.assertEqual(result, 'Hello, UTF-8 world!\n')
def test_open_text_given_encoding(self):
target = resources.files(self.data) / 'utf-16.file'
with target.open(encoding='utf-16', errors='strict') as fp:
result = fp.read()
self.assertEqual(result, 'Hello, UTF-16 world!\n')
def test_open_text_with_errors(self):
"""
Raises UnicodeError without the 'errors' argument.
"""
target = resources.files(self.data) / 'utf-16.file'
with target.open(encoding='utf-8', errors='strict') as fp:
self.assertRaises(UnicodeError, fp.read)
with target.open(encoding='utf-8', errors='ignore') as fp:
result = fp.read()
self.assertEqual(
result,
'H\x00e\x00l\x00l\x00o\x00,\x00 '
'\x00U\x00T\x00F\x00-\x001\x006\x00 '
'\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00',
)
def test_open_binary_FileNotFoundError(self):
target = resources.files(self.data) / 'does-not-exist'
with self.assertRaises(FileNotFoundError):
target.open('rb')
def test_open_text_FileNotFoundError(self):
target = resources.files(self.data) / 'does-not-exist'
with self.assertRaises(FileNotFoundError):
target.open(encoding='utf-8')
class OpenDiskTests(OpenTests, unittest.TestCase):
def setUp(self):
self.data = data01
class OpenDiskNamespaceTests(OpenTests, unittest.TestCase):
def setUp(self):
from . import namespacedata01
self.data = namespacedata01
class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
pass
class OpenNamespaceZipTests(OpenTests, util.ZipSetup, unittest.TestCase):
ZIP_MODULE = 'namespacedata01'
if __name__ == '__main__':
unittest.main()
import io
import pathlib
import unittest
import importlib_resources as resources
from . import data01
from . import util
class CommonTests(util.CommonTests, unittest.TestCase):
def execute(self, package, path):
with resources.as_file(resources.files(package).joinpath(path)):
pass
class PathTests:
def test_reading(self):
"""
Path should be readable and a pathlib.Path instance.
"""
target = resources.files(self.data) / 'utf-8.file'
with resources.as_file(target) as path:
self.assertIsInstance(path, pathlib.Path)
self.assertTrue(path.name.endswith("utf-8.file"), repr(path))
self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8'))
class PathDiskTests(PathTests, unittest.TestCase):
data = data01
def test_natural_path(self):
"""
Guarantee the internal implementation detail that
file-system-backed resources do not get the tempdir
treatment.
"""
target = resources.files(self.data) / 'utf-8.file'
with resources.as_file(target) as path:
assert 'data' in str(path)
class PathMemoryTests(PathTests, unittest.TestCase):
def setUp(self):
file = io.BytesIO(b'Hello, UTF-8 world!\n')
self.addCleanup(file.close)
self.data = util.create_package(
file=file, path=FileNotFoundError("package exists only in memory")
)
self.data.__spec__.origin = None
self.data.__spec__.has_location = False
class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase):
def test_remove_in_context_manager(self):
"""
It is not an error if the file that was temporarily stashed on the
file system is removed inside the `with` stanza.
"""
target = resources.files(self.data) / 'utf-8.file'
with resources.as_file(target) as path:
path.unlink()
if __name__ == '__main__':
unittest.main()
import unittest
import importlib_resources as resources
from . import data01
from . import util
from importlib import import_module
class CommonBinaryTests(util.CommonTests, unittest.TestCase):
def execute(self, package, path):
resources.files(package).joinpath(path).read_bytes()
class CommonTextTests(util.CommonTests, unittest.TestCase):
def execute(self, package, path):
resources.files(package).joinpath(path).read_text(encoding='utf-8')
class ReadTests:
def test_read_bytes(self):
result = resources.files(self.data).joinpath('binary.file').read_bytes()
self.assertEqual(result, bytes(range(4)))
def test_read_text_default_encoding(self):
result = (
resources.files(self.data)
.joinpath('utf-8.file')
.read_text(encoding='utf-8')
)
self.assertEqual(result, 'Hello, UTF-8 world!\n')
def test_read_text_given_encoding(self):
result = (
resources.files(self.data)
.joinpath('utf-16.file')
.read_text(encoding='utf-16')
)
self.assertEqual(result, 'Hello, UTF-16 world!\n')
def test_read_text_with_errors(self):
"""
Raises UnicodeError without the 'errors' argument.
"""
target = resources.files(self.data) / 'utf-16.file'
self.assertRaises(UnicodeError, target.read_text, encoding='utf-8')
result = target.read_text(encoding='utf-8', errors='ignore')
self.assertEqual(
result,
'H\x00e\x00l\x00l\x00o\x00,\x00 '
'\x00U\x00T\x00F\x00-\x001\x006\x00 '
'\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00',
)
class ReadDiskTests(ReadTests, unittest.TestCase):
data = data01
class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
def test_read_submodule_resource(self):
submodule = import_module('data01.subdirectory')
result = resources.files(submodule).joinpath('binary.file').read_bytes()
self.assertEqual(result, bytes(range(4, 8)))
def test_read_submodule_resource_by_name(self):
result = (
resources.files('data01.subdirectory').joinpath('binary.file').read_bytes()
)
self.assertEqual(result, bytes(range(4, 8)))
class ReadNamespaceTests(ReadTests, unittest.TestCase):
def setUp(self):
from . import namespacedata01
self.data = namespacedata01
class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase):
ZIP_MODULE = 'namespacedata01'
def test_read_submodule_resource(self):
submodule = import_module('namespacedata01.subdirectory')
result = resources.files(submodule).joinpath('binary.file').read_bytes()
self.assertEqual(result, bytes(range(12, 16)))
def test_read_submodule_resource_by_name(self):
result = (
resources.files('namespacedata01.subdirectory')
.joinpath('binary.file')
.read_bytes()
)
self.assertEqual(result, bytes(range(12, 16)))
if __name__ == '__main__':
unittest.main()
import os.path
import sys
import pathlib
import unittest
from importlib import import_module
from importlib_resources.readers import MultiplexedPath, NamespaceReader
class MultiplexedPathTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.folder = pathlib.Path(__file__).parent / 'namespacedata01'
def test_init_no_paths(self):
with self.assertRaises(FileNotFoundError):
MultiplexedPath()
def test_init_file(self):
with self.assertRaises(NotADirectoryError):
MultiplexedPath(self.folder / 'binary.file')
def test_iterdir(self):
contents = {path.name for path in MultiplexedPath(self.folder).iterdir()}
try:
contents.remove('__pycache__')
except (KeyError, ValueError):
pass
self.assertEqual(
contents, {'subdirectory', 'binary.file', 'utf-16.file', 'utf-8.file'}
)
def test_iterdir_duplicate(self):
data01 = pathlib.Path(__file__).parent.joinpath('data01')
contents = {
path.name for path in MultiplexedPath(self.folder, data01).iterdir()
}
for remove in ('__pycache__', '__init__.pyc'):
try:
contents.remove(remove)
except (KeyError, ValueError):
pass
self.assertEqual(
contents,
{'__init__.py', 'binary.file', 'subdirectory', 'utf-16.file', 'utf-8.file'},
)
def test_is_dir(self):
self.assertEqual(MultiplexedPath(self.folder).is_dir(), True)
def test_is_file(self):
self.assertEqual(MultiplexedPath(self.folder).is_file(), False)
def test_open_file(self):
path = MultiplexedPath(self.folder)
with self.assertRaises(FileNotFoundError):
path.read_bytes()
with self.assertRaises(FileNotFoundError):
path.read_text()
with self.assertRaises(FileNotFoundError):
path.open()
def test_join_path(self):
data01 = pathlib.Path(__file__).parent.joinpath('data01')
prefix = str(data01.parent)
path = MultiplexedPath(self.folder, data01)
self.assertEqual(
str(path.joinpath('binary.file'))[len(prefix) + 1 :],
os.path.join('namespacedata01', 'binary.file'),
)
sub = path.joinpath('subdirectory')
assert isinstance(sub, MultiplexedPath)
assert 'namespacedata01' in str(sub)
assert 'data01' in str(sub)
self.assertEqual(
str(path.joinpath('imaginary'))[len(prefix) + 1 :],
os.path.join('namespacedata01', 'imaginary'),
)
self.assertEqual(path.joinpath(), path)
def test_join_path_compound(self):
path = MultiplexedPath(self.folder)
assert not path.joinpath('imaginary/foo.py').exists()
def test_join_path_common_subdir(self):
data01 = pathlib.Path(__file__).parent.joinpath('data01')
data02 = pathlib.Path(__file__).parent.joinpath('data02')
prefix = str(data01.parent)
path = MultiplexedPath(data01, data02)
self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath)
self.assertEqual(
str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :],
os.path.join('data02', 'subdirectory', 'subsubdir'),
)
def test_repr(self):
self.assertEqual(
repr(MultiplexedPath(self.folder)),
f"MultiplexedPath('{self.folder}')",
)
def test_name(self):
self.assertEqual(
MultiplexedPath(self.folder).name,
os.path.basename(self.folder),
)
class NamespaceReaderTest(unittest.TestCase):
site_dir = str(pathlib.Path(__file__).parent)
@classmethod
def setUpClass(cls):
sys.path.append(cls.site_dir)
@classmethod
def tearDownClass(cls):
sys.path.remove(cls.site_dir)
def test_init_error(self):
with self.assertRaises(ValueError):
NamespaceReader(['path1', 'path2'])
def test_resource_path(self):
namespacedata01 = import_module('namespacedata01')
reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations)
root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01'))
self.assertEqual(
reader.resource_path('binary.file'), os.path.join(root, 'binary.file')
)
self.assertEqual(
reader.resource_path('imaginary'), os.path.join(root, 'imaginary')
)
def test_files(self):
namespacedata01 = import_module('namespacedata01')
reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations)
root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01'))
self.assertIsInstance(reader.files(), MultiplexedPath)
self.assertEqual(repr(reader.files()), f"MultiplexedPath('{root}')")
if __name__ == '__main__':
unittest.main()
import sys
import unittest
import importlib_resources as resources
import pathlib
from . import data01
from . import util
from importlib import import_module
class ResourceTests:
# Subclasses are expected to set the `data` attribute.
def test_is_file_exists(self):
target = resources.files(self.data) / 'binary.file'
self.assertTrue(target.is_file())
def test_is_file_missing(self):
target = resources.files(self.data) / 'not-a-file'
self.assertFalse(target.is_file())
def test_is_dir(self):
target = resources.files(self.data) / 'subdirectory'
self.assertFalse(target.is_file())
self.assertTrue(target.is_dir())
class ResourceDiskTests(ResourceTests, unittest.TestCase):
def setUp(self):
self.data = data01
class ResourceZipTests(ResourceTests, util.ZipSetup, unittest.TestCase):
pass
def names(traversable):
return {item.name for item in traversable.iterdir()}
class ResourceLoaderTests(unittest.TestCase):
def test_resource_contents(self):
package = util.create_package(
file=data01, path=data01.__file__, contents=['A', 'B', 'C']
)
self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'})
def test_is_file(self):
package = util.create_package(
file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']
)
self.assertTrue(resources.files(package).joinpath('B').is_file())
def test_is_dir(self):
package = util.create_package(
file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']
)
self.assertTrue(resources.files(package).joinpath('D').is_dir())
def test_resource_missing(self):
package = util.create_package(
file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']
)
self.assertFalse(resources.files(package).joinpath('Z').is_file())
class ResourceCornerCaseTests(unittest.TestCase):
def test_package_has_no_reader_fallback(self):
"""
Test odd ball packages which:
# 1. Do not have a ResourceReader as a loader
# 2. Are not on the file system
# 3. Are not in a zip file
"""
module = util.create_package(
file=data01, path=data01.__file__, contents=['A', 'B', 'C']
)
# Give the module a dummy loader.
module.__loader__ = object()
# Give the module a dummy origin.
module.__file__ = '/path/which/shall/not/be/named'
module.__spec__.loader = module.__loader__
module.__spec__.origin = module.__file__
self.assertFalse(resources.files(module).joinpath('A').is_file())
class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):
ZIP_MODULE = 'data01'
def test_is_submodule_resource(self):
submodule = import_module('data01.subdirectory')
self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file())
def test_read_submodule_resource_by_name(self):
self.assertTrue(
resources.files('data01.subdirectory').joinpath('binary.file').is_file()
)
def test_submodule_contents(self):
submodule = import_module('data01.subdirectory')
self.assertEqual(
names(resources.files(submodule)), {'__init__.py', 'binary.file'}
)
def test_submodule_contents_by_name(self):
self.assertEqual(
names(resources.files('data01.subdirectory')),
{'__init__.py', 'binary.file'},
)
def test_as_file_directory(self):
with resources.as_file(resources.files('data01')) as data:
assert data.name == 'data01'
assert data.is_dir()
assert data.joinpath('subdirectory').is_dir()
assert len(list(data.iterdir()))
assert not data.parent.exists()
class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase):
ZIP_MODULE = 'data02'
def test_unrelated_contents(self):
"""
Test thata zip with two unrelated subpackages return
distinct resources. Ref python/importlib_resources#44.
"""
self.assertEqual(
names(resources.files('data02.one')),
{'__init__.py', 'resource1.txt'},
)
self.assertEqual(
names(resources.files('data02.two')),
{'__init__.py', 'resource2.txt'},
)
class DeletingZipsTest(util.ZipSetupBase, unittest.TestCase):
"""Having accessed resources in a zip file should not keep an open
reference to the zip.
"""
def test_iterdir_does_not_keep_open(self):
[item.name for item in resources.files('data01').iterdir()]
def test_is_file_does_not_keep_open(self):
resources.files('data01').joinpath('binary.file').is_file()
def test_is_file_failure_does_not_keep_open(self):
resources.files('data01').joinpath('not-present').is_file()
@unittest.skip("Desired but not supported.")
def test_as_file_does_not_keep_open(self): # pragma: no cover
resources.as_file(resources.files('data01') / 'binary.file')
def test_entered_path_does_not_keep_open(self):
"""
Mimic what certifi does on import to make its bundle
available for the process duration.
"""
resources.as_file(resources.files('data01') / 'binary.file').__enter__()
def test_read_binary_does_not_keep_open(self):
resources.files('data01').joinpath('binary.file').read_bytes()
def test_read_text_does_not_keep_open(self):
resources.files('data01').joinpath('utf-8.file').read_text(encoding='utf-8')
class ResourceFromNamespaceTests:
def test_is_submodule_resource(self):
self.assertTrue(
resources.files(import_module('namespacedata01'))
.joinpath('binary.file')
.is_file()
)
def test_read_submodule_resource_by_name(self):
self.assertTrue(
resources.files('namespacedata01').joinpath('binary.file').is_file()
)
def test_submodule_contents(self):
contents = names(resources.files(import_module('namespacedata01')))
try:
contents.remove('__pycache__')
except KeyError:
pass
self.assertEqual(
contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}
)
def test_submodule_contents_by_name(self):
contents = names(resources.files('namespacedata01'))
try:
contents.remove('__pycache__')
except KeyError:
pass
self.assertEqual(
contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}
)
def test_submodule_sub_contents(self):
contents = names(resources.files(import_module('namespacedata01.subdirectory')))
try:
contents.remove('__pycache__')
except KeyError:
pass
self.assertEqual(contents, {'binary.file'})
def test_submodule_sub_contents_by_name(self):
contents = names(resources.files('namespacedata01.subdirectory'))
try:
contents.remove('__pycache__')
except KeyError:
pass
self.assertEqual(contents, {'binary.file'})
class ResourceFromNamespaceDiskTests(ResourceFromNamespaceTests, unittest.TestCase):
site_dir = str(pathlib.Path(__file__).parent)
@classmethod
def setUpClass(cls):
sys.path.append(cls.site_dir)
@classmethod
def tearDownClass(cls):
sys.path.remove(cls.site_dir)
class ResourceFromNamespaceZipTests(
util.ZipSetupBase,
ResourceFromNamespaceTests,
unittest.TestCase,
):
ZIP_MODULE = 'namespacedata01'
if __name__ == '__main__':
unittest.main()
import abc
import importlib
import io
import sys
import types
import pathlib
import contextlib
from . import data01
from ..abc import ResourceReader
from .compat.py39 import import_helper, os_helper
from . import zip as zip_
from importlib.machinery import ModuleSpec
class Reader(ResourceReader):
def __init__(self, **kwargs):
vars(self).update(kwargs)
def get_resource_reader(self, package):
return self
def open_resource(self, path):
self._path = path
if isinstance(self.file, Exception):
raise self.file
return self.file
def resource_path(self, path_):
self._path = path_
if isinstance(self.path, Exception):
raise self.path
return self.path
def is_resource(self, path_):
self._path = path_
if isinstance(self.path, Exception):
raise self.path
def part(entry):
return entry.split('/')
return any(
len(parts) == 1 and parts[0] == path_ for parts in map(part, self._contents)
)
def contents(self):
if isinstance(self.path, Exception):
raise self.path
yield from self._contents
def create_package_from_loader(loader, is_package=True):
name = 'testingpackage'
module = types.ModuleType(name)
spec = ModuleSpec(name, loader, origin='does-not-exist', is_package=is_package)
module.__spec__ = spec
module.__loader__ = loader
return module
def create_package(file=None, path=None, is_package=True, contents=()):
return create_package_from_loader(
Reader(file=file, path=path, _contents=contents),
is_package,
)
class CommonTests(metaclass=abc.ABCMeta):
"""
Tests shared by test_open, test_path, and test_read.
"""
@abc.abstractmethod
def execute(self, package, path):
"""
Call the pertinent legacy API function (e.g. open_text, path)
on package and path.
"""
def test_package_name(self):
"""
Passing in the package name should succeed.
"""
self.execute(data01.__name__, 'utf-8.file')
def test_package_object(self):
"""
Passing in the package itself should succeed.
"""
self.execute(data01, 'utf-8.file')
def test_string_path(self):
"""
Passing in a string for the path should succeed.
"""
path = 'utf-8.file'
self.execute(data01, path)
def test_pathlib_path(self):
"""
Passing in a pathlib.PurePath object for the path should succeed.
"""
path = pathlib.PurePath('utf-8.file')
self.execute(data01, path)
def test_importing_module_as_side_effect(self):
"""
The anchor package can already be imported.
"""
del sys.modules[data01.__name__]
self.execute(data01.__name__, 'utf-8.file')
def test_missing_path(self):
"""
Attempting to open or read or request the path for a
non-existent path should succeed if open_resource
can return a viable data stream.
"""
bytes_data = io.BytesIO(b'Hello, world!')
package = create_package(file=bytes_data, path=FileNotFoundError())
self.execute(package, 'utf-8.file')
self.assertEqual(package.__loader__._path, 'utf-8.file')
def test_extant_path(self):
# Attempting to open or read or request the path when the
# path does exist should still succeed. Does not assert
# anything about the result.
bytes_data = io.BytesIO(b'Hello, world!')
# any path that exists
path = __file__
package = create_package(file=bytes_data, path=path)
self.execute(package, 'utf-8.file')
self.assertEqual(package.__loader__._path, 'utf-8.file')
def test_useless_loader(self):
package = create_package(file=FileNotFoundError(), path=FileNotFoundError())
with self.assertRaises(FileNotFoundError):
self.execute(package, 'utf-8.file')
class ZipSetupBase:
ZIP_MODULE = 'data01'
def setUp(self):
self.fixtures = contextlib.ExitStack()
self.addCleanup(self.fixtures.close)
self.fixtures.enter_context(import_helper.isolated_modules())
temp_dir = self.fixtures.enter_context(os_helper.temp_dir())
modules = pathlib.Path(temp_dir) / 'zipped modules.zip'
src_path = pathlib.Path(__file__).parent.joinpath(self.ZIP_MODULE)
self.fixtures.enter_context(
import_helper.DirsOnSysPath(str(zip_.make_zip_file(src_path, modules)))
)
self.data = importlib.import_module(self.ZIP_MODULE)
class ZipSetup(ZipSetupBase):
pass
"""
Generate zip test data files.
"""
import contextlib
import os
import pathlib
import zipfile
import zipp
def make_zip_file(src, dst):
"""
Zip the files in src into a new zipfile at dst.
"""
with zipfile.ZipFile(dst, 'w') as zf:
for src_path, rel in walk(src):
dst_name = src.name / pathlib.PurePosixPath(rel.as_posix())
zf.write(src_path, dst_name)
zipp.CompleteDirs.inject(zf)
return dst
def walk(datapath):
for dirpath, dirnames, filenames in os.walk(datapath):
with contextlib.suppress(ValueError):
dirnames.remove('__pycache__')
for filename in filenames:
res = pathlib.Path(dirpath) / filename
rel = res.relative_to(datapath)
yield res, rel

Sorry, the diff of this file is not supported yet

This software is made available under the terms of *either* of the licenses
found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
under the terms of *both* these licenses.

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Metadata-Version: 2.1
Name: packaging
Version: 24.1
Summary: Core utilities for Python packages
Author-email: Donald Stufft <donald@stufft.io>
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Typing :: Typed
Project-URL: Documentation, https://packaging.pypa.io/
Project-URL: Source, https://github.com/pypa/packaging
packaging
=========
.. start-intro
Reusable core utilities for various Python Packaging
`interoperability specifications <https://packaging.python.org/specifications/>`_.
This library provides utilities that implement the interoperability
specifications which have clearly one correct behaviour (eg: :pep:`440`)
or benefit greatly from having a single shared implementation (eg: :pep:`425`).
.. end-intro
The ``packaging`` project includes the following: version handling, specifiers,
markers, requirements, tags, utilities.
Documentation
-------------
The `documentation`_ provides information and the API for the following:
- Version Handling
- Specifiers
- Markers
- Requirements
- Tags
- Utilities
Installation
------------
Use ``pip`` to install these utilities::
pip install packaging
The ``packaging`` library uses calendar-based versioning (``YY.N``).
Discussion
----------
If you run into bugs, you can file them in our `issue tracker`_.
You can also join ``#pypa`` on Freenode to ask questions or get involved.
.. _`documentation`: https://packaging.pypa.io/
.. _`issue tracker`: https://github.com/pypa/packaging/issues
Code of Conduct
---------------
Everyone interacting in the packaging project's codebases, issue trackers, chat
rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
Contributing
------------
The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as
well as how to report a potential security issue. The documentation for this
project also covers information about `project development`_ and `security`_.
.. _`project development`: https://packaging.pypa.io/en/latest/development/
.. _`security`: https://packaging.pypa.io/en/latest/security/
Project History
---------------
Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for
recent changes and project history.
.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/
packaging-24.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
packaging-24.1.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
packaging-24.1.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
packaging-24.1.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
packaging-24.1.dist-info/METADATA,sha256=X3ooO3WnCfzNSBrqQjefCD1POAF1M2WSLmsHMgQlFdk,3204
packaging-24.1.dist-info/RECORD,,
packaging-24.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
packaging-24.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
packaging/__init__.py,sha256=dtw2bNmWCQ9WnMoK3bk_elL1svSlikXtLpZhCFIB9SE,496
packaging/__pycache__/__init__.cpython-312.pyc,,
packaging/__pycache__/_elffile.cpython-312.pyc,,
packaging/__pycache__/_manylinux.cpython-312.pyc,,
packaging/__pycache__/_musllinux.cpython-312.pyc,,
packaging/__pycache__/_parser.cpython-312.pyc,,
packaging/__pycache__/_structures.cpython-312.pyc,,
packaging/__pycache__/_tokenizer.cpython-312.pyc,,
packaging/__pycache__/markers.cpython-312.pyc,,
packaging/__pycache__/metadata.cpython-312.pyc,,
packaging/__pycache__/requirements.cpython-312.pyc,,
packaging/__pycache__/specifiers.cpython-312.pyc,,
packaging/__pycache__/tags.cpython-312.pyc,,
packaging/__pycache__/utils.cpython-312.pyc,,
packaging/__pycache__/version.cpython-312.pyc,,
packaging/_elffile.py,sha256=_LcJW4YNKywYsl4169B2ukKRqwxjxst_8H0FRVQKlz8,3282
packaging/_manylinux.py,sha256=Xo4V0PZz8sbuVCbTni0t1CR0AHeir_7ib4lTmV8scD4,9586
packaging/_musllinux.py,sha256=p9ZqNYiOItGee8KcZFeHF_YcdhVwGHdK6r-8lgixvGQ,2694
packaging/_parser.py,sha256=s_TvTvDNK0NrM2QB3VKThdWFM4Nc0P6JnkObkl3MjpM,10236
packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431
packaging/_tokenizer.py,sha256=J6v5H7Jzvb-g81xp_2QACKwO7LxHQA6ikryMU7zXwN8,5273
packaging/markers.py,sha256=dWKSqn5Sp-jDmOG-W3GfLHKjwhf1IsznbT71VlBoB5M,10671
packaging/metadata.py,sha256=KINuSkJ12u-SyoKNTy_pHNGAfMUtxNvZ53qA1zAKcKI,32349
packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
packaging/requirements.py,sha256=gYyRSAdbrIyKDY66ugIDUQjRMvxkH2ALioTmX3tnL6o,2947
packaging/specifiers.py,sha256=rjpc3hoJuunRIT6DdH7gLTnQ5j5QKSuWjoTC5sdHtHI,39714
packaging/tags.py,sha256=y8EbheOu9WS7s-MebaXMcHMF-jzsA_C1Lz5XRTiSy4w,18883
packaging/utils.py,sha256=NAdYUwnlAOpkat_RthavX8a07YuVxgGL_vwrx73GSDM,5287
packaging/version.py,sha256=V0H3SOj_8werrvorrb6QDLRhlcqSErNTTkCvvfszhDI,16198

Sorry, the diff of this file is not supported yet

Wheel-Version: 1.0
Generator: flit 3.9.0
Root-Is-Purelib: true
Tag: py3-none-any
[console_scripts]
wheel=wheel.cli:main
[distutils.commands]
bdist_wheel=wheel.bdist_wheel:bdist_wheel

Sorry, the diff of this file is not supported yet

MIT License
Copyright (c) 2012 Daniel Holth <dholth@fastmail.fm> and contributors
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.
Metadata-Version: 2.1
Name: wheel
Version: 0.43.0
Summary: A built-package format for Python
Keywords: wheel,packaging
Author-email: Daniel Holth <dholth@fastmail.fm>
Maintainer-email: Alex Grönholm <alex.gronholm@nextday.fi>
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Topic :: System :: Archiving :: Packaging
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Dist: pytest >= 6.0.0 ; extra == "test"
Requires-Dist: setuptools >= 65 ; extra == "test"
Project-URL: Changelog, https://wheel.readthedocs.io/en/stable/news.html
Project-URL: Documentation, https://wheel.readthedocs.io/
Project-URL: Issue Tracker, https://github.com/pypa/wheel/issues
Project-URL: Source, https://github.com/pypa/wheel
Provides-Extra: test
wheel
=====
This library is the reference implementation of the Python wheel packaging
standard, as defined in `PEP 427`_.
It has two different roles:
#. A setuptools_ extension for building wheels that provides the
``bdist_wheel`` setuptools command
#. A command line tool for working with wheel files
It should be noted that wheel is **not** intended to be used as a library, and
as such there is no stable, public API.
.. _PEP 427: https://www.python.org/dev/peps/pep-0427/
.. _setuptools: https://pypi.org/project/setuptools/
Documentation
-------------
The documentation_ can be found on Read The Docs.
.. _documentation: https://wheel.readthedocs.io/
Code of Conduct
---------------
Everyone interacting in the wheel project's codebases, issue trackers, chat
rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
../../bin/wheel,sha256=cT2EHbrv-J-UyUXu26cDY-0I7RgcruysJeHFanT1Xfo,249
wheel-0.43.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
wheel-0.43.0.dist-info/LICENSE.txt,sha256=MMI2GGeRCPPo6h0qZYx8pBe9_IkcmO8aifpP8MmChlQ,1107
wheel-0.43.0.dist-info/METADATA,sha256=WbrCKwClnT5WCKVrjPjvxDgxo2tyeS7kOJyc1GaceEE,2153
wheel-0.43.0.dist-info/RECORD,,
wheel-0.43.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
wheel-0.43.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
wheel-0.43.0.dist-info/entry_points.txt,sha256=rTY1BbkPHhkGMm4Q3F0pIzJBzW2kMxoG1oriffvGdA0,104
wheel/__init__.py,sha256=D6jhH00eMzbgrXGAeOwVfD5i-lCAMMycuG1L0useDlo,59
wheel/__main__.py,sha256=NkMUnuTCGcOkgY0IBLgBCVC_BGGcWORx2K8jYGS12UE,455
wheel/__pycache__/__init__.cpython-312.pyc,,
wheel/__pycache__/__main__.cpython-312.pyc,,
wheel/__pycache__/_setuptools_logging.cpython-312.pyc,,
wheel/__pycache__/bdist_wheel.cpython-312.pyc,,
wheel/__pycache__/macosx_libfile.cpython-312.pyc,,
wheel/__pycache__/metadata.cpython-312.pyc,,
wheel/__pycache__/util.cpython-312.pyc,,
wheel/__pycache__/wheelfile.cpython-312.pyc,,
wheel/_setuptools_logging.py,sha256=NoCnjJ4DFEZ45Eo-2BdXLsWJCwGkait1tp_17paleVw,746
wheel/bdist_wheel.py,sha256=OKJyp9E831zJrxoRfmM9AgOjByG1CB-pzF5kXQFmaKk,20938
wheel/cli/__init__.py,sha256=eBNhnPwWTtdKAJHy77lvz7gOQ5Eu3GavGugXxhSsn-U,4264
wheel/cli/__pycache__/__init__.cpython-312.pyc,,
wheel/cli/__pycache__/convert.cpython-312.pyc,,
wheel/cli/__pycache__/pack.cpython-312.pyc,,
wheel/cli/__pycache__/tags.cpython-312.pyc,,
wheel/cli/__pycache__/unpack.cpython-312.pyc,,
wheel/cli/convert.py,sha256=qJcpYGKqdfw1P6BelgN1Hn_suNgM6bvyEWFlZeuSWx0,9439
wheel/cli/pack.py,sha256=CAFcHdBVulvsHYJlndKVO7KMI9JqBTZz5ii0PKxxCOs,3103
wheel/cli/tags.py,sha256=lHw-LaWrkS5Jy_qWcw-6pSjeNM6yAjDnqKI3E5JTTCU,4760
wheel/cli/unpack.py,sha256=Y_J7ynxPSoFFTT7H0fMgbBlVErwyDGcObgme5MBuz58,1021
wheel/macosx_libfile.py,sha256=HnW6OPdN993psStvwl49xtx2kw7hoVbe6nvwmf8WsKI,16103
wheel/metadata.py,sha256=q-xCCqSAK7HzyZxK9A6_HAWmhqS1oB4BFw1-rHQxBiQ,5884
wheel/util.py,sha256=e0jpnsbbM9QhaaMSyap-_ZgUxcxwpyLDk6RHcrduPLg,621
wheel/vendored/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
wheel/vendored/__pycache__/__init__.cpython-312.pyc,,
wheel/vendored/packaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
wheel/vendored/packaging/__pycache__/__init__.cpython-312.pyc,,
wheel/vendored/packaging/__pycache__/_elffile.cpython-312.pyc,,
wheel/vendored/packaging/__pycache__/_manylinux.cpython-312.pyc,,
wheel/vendored/packaging/__pycache__/_musllinux.cpython-312.pyc,,
wheel/vendored/packaging/__pycache__/_parser.cpython-312.pyc,,
wheel/vendored/packaging/__pycache__/_structures.cpython-312.pyc,,
wheel/vendored/packaging/__pycache__/_tokenizer.cpython-312.pyc,,
wheel/vendored/packaging/__pycache__/markers.cpython-312.pyc,,
wheel/vendored/packaging/__pycache__/requirements.cpython-312.pyc,,
wheel/vendored/packaging/__pycache__/specifiers.cpython-312.pyc,,
wheel/vendored/packaging/__pycache__/tags.cpython-312.pyc,,
wheel/vendored/packaging/__pycache__/utils.cpython-312.pyc,,
wheel/vendored/packaging/__pycache__/version.cpython-312.pyc,,
wheel/vendored/packaging/_elffile.py,sha256=hbmK8OD6Z7fY6hwinHEUcD1by7czkGiNYu7ShnFEk2k,3266
wheel/vendored/packaging/_manylinux.py,sha256=P7sdR5_7XBY09LVYYPhHmydMJIIwPXWsh4olk74Uuj4,9588
wheel/vendored/packaging/_musllinux.py,sha256=z1s8To2hQ0vpn_d-O2i5qxGwEK8WmGlLt3d_26V7NeY,2674
wheel/vendored/packaging/_parser.py,sha256=4tT4emSl2qTaU7VTQE1Xa9o1jMPCsBezsYBxyNMUN-s,10347
wheel/vendored/packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431
wheel/vendored/packaging/_tokenizer.py,sha256=alCtbwXhOFAmFGZ6BQ-wCTSFoRAJ2z-ysIf7__MTJ_k,5292
wheel/vendored/packaging/markers.py,sha256=_TSPI1BhJYO7Bp9AzTmHQxIqHEVXaTjmDh9G-w8qzPA,8232
wheel/vendored/packaging/requirements.py,sha256=dgoBeVprPu2YE6Q8nGfwOPTjATHbRa_ZGLyXhFEln6Q,2933
wheel/vendored/packaging/specifiers.py,sha256=IWSt0SrLSP72heWhAC8UL0eGvas7XIQHjqiViVfmPKE,39778
wheel/vendored/packaging/tags.py,sha256=fedHXiOHkBxNZTXotXv8uXPmMFU9ae-TKBujgYHigcA,18950
wheel/vendored/packaging/utils.py,sha256=XgdmP3yx9-wQEFjO7OvMj9RjEf5JlR5HFFR69v7SQ9E,5268
wheel/vendored/packaging/version.py,sha256=PFJaYZDxBgyxkfYhH3SQw4qfE9ICCWrTmitvq14y3bs,16234
wheel/vendored/vendor.txt,sha256=Z2ENjB1i5prfez8CdM1Sdr3c6Zxv2rRRolMpLmBncAE,16
wheel/wheelfile.py,sha256=DtJDWoZMvnBh4leNMDPGOprQU9d_dp6q-MmV0U--4xc,7694

Sorry, the diff of this file is not supported yet

Wheel-Version: 1.0
Generator: flit 3.9.0
Root-Is-Purelib: true
Tag: py3-none-any
"""PyPI and direct package downloading."""
import base64
import configparser
import hashlib
import html
import http.client
import io
import itertools
import os
import re
import shutil
import socket
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
from fnmatch import translate
from functools import wraps
from typing import NamedTuple
from more_itertools import unique_everseen
import setuptools
from pkg_resources import (
BINARY_DIST,
CHECKOUT_DIST,
DEVELOP_DIST,
EGG_DIST,
SOURCE_DIST,
Distribution,
Environment,
Requirement,
find_distributions,
normalize_path,
parse_version,
safe_name,
safe_version,
to_filename,
)
from setuptools.wheel import Wheel
from .unicode_utils import _cfg_read_utf8_with_fallback, _read_utf8_with_fallback
from distutils import log
from distutils.errors import DistutilsError
EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$')
HREF = re.compile(r"""href\s*=\s*['"]?([^'"> ]+)""", re.I)
PYPI_MD5 = re.compile(
r'<a href="([^"#]+)">([^<]+)</a>\n\s+\(<a (?:title="MD5 hash"\n\s+)'
r'href="[^?]+\?:action=show_md5&amp;digest=([0-9a-f]{32})">md5</a>\)'
)
URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match
EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz".split()
__all__ = [
'PackageIndex',
'distros_for_url',
'parse_bdist_wininst',
'interpret_distro_name',
]
_SOCKET_TIMEOUT = 15
_tmpl = "setuptools/{setuptools.__version__} Python-urllib/{py_major}"
user_agent = _tmpl.format(
py_major='{}.{}'.format(*sys.version_info), setuptools=setuptools
)
def parse_requirement_arg(spec):
try:
return Requirement.parse(spec)
except ValueError as e:
raise DistutilsError(
"Not a URL, existing file, or requirement spec: %r" % (spec,)
) from e
def parse_bdist_wininst(name):
"""Return (base,pyversion) or (None,None) for possible .exe name"""
lower = name.lower()
base, py_ver, plat = None, None, None
if lower.endswith('.exe'):
if lower.endswith('.win32.exe'):
base = name[:-10]
plat = 'win32'
elif lower.startswith('.win32-py', -16):
py_ver = name[-7:-4]
base = name[:-16]
plat = 'win32'
elif lower.endswith('.win-amd64.exe'):
base = name[:-14]
plat = 'win-amd64'
elif lower.startswith('.win-amd64-py', -20):
py_ver = name[-7:-4]
base = name[:-20]
plat = 'win-amd64'
return base, py_ver, plat
def egg_info_for_url(url):
parts = urllib.parse.urlparse(url)
scheme, server, path, parameters, query, fragment = parts
base = urllib.parse.unquote(path.split('/')[-1])
if server == 'sourceforge.net' and base == 'download': # XXX Yuck
base = urllib.parse.unquote(path.split('/')[-2])
if '#' in base:
base, fragment = base.split('#', 1)
return base, fragment
def distros_for_url(url, metadata=None):
"""Yield egg or source distribution objects that might be found at a URL"""
base, fragment = egg_info_for_url(url)
yield from distros_for_location(url, base, metadata)
if fragment:
match = EGG_FRAGMENT.match(fragment)
if match:
yield from interpret_distro_name(
url, match.group(1), metadata, precedence=CHECKOUT_DIST
)
def distros_for_location(location, basename, metadata=None):
"""Yield egg or source distribution objects based on basename"""
if basename.endswith('.egg.zip'):
basename = basename[:-4] # strip the .zip
if basename.endswith('.egg') and '-' in basename:
# only one, unambiguous interpretation
return [Distribution.from_location(location, basename, metadata)]
if basename.endswith('.whl') and '-' in basename:
wheel = Wheel(basename)
if not wheel.is_compatible():
return []
return [
Distribution(
location=location,
project_name=wheel.project_name,
version=wheel.version,
# Increase priority over eggs.
precedence=EGG_DIST + 1,
)
]
if basename.endswith('.exe'):
win_base, py_ver, platform = parse_bdist_wininst(basename)
if win_base is not None:
return interpret_distro_name(
location, win_base, metadata, py_ver, BINARY_DIST, platform
)
# Try source distro extensions (.zip, .tgz, etc.)
#
for ext in EXTENSIONS:
if basename.endswith(ext):
basename = basename[: -len(ext)]
return interpret_distro_name(location, basename, metadata)
return [] # no extension matched
def distros_for_filename(filename, metadata=None):
"""Yield possible egg or source distribution objects based on a filename"""
return distros_for_location(
normalize_path(filename), os.path.basename(filename), metadata
)
def interpret_distro_name(
location, basename, metadata, py_version=None, precedence=SOURCE_DIST, platform=None
):
"""Generate the interpretation of a source distro name
Note: if `location` is a filesystem filename, you should call
``pkg_resources.normalize_path()`` on it before passing it to this
routine!
"""
parts = basename.split('-')
if not py_version and any(re.match(r'py\d\.\d$', p) for p in parts[2:]):
# it is a bdist_dumb, not an sdist -- bail out
return
# find the pivot (p) that splits the name from the version.
# infer the version as the first item that has a digit.
for p in range(len(parts)):
if parts[p][:1].isdigit():
break
else:
p = len(parts)
yield Distribution(
location,
metadata,
'-'.join(parts[:p]),
'-'.join(parts[p:]),
py_version=py_version,
precedence=precedence,
platform=platform,
)
def unique_values(func):
"""
Wrap a function returning an iterable such that the resulting iterable
only ever yields unique items.
"""
@wraps(func)
def wrapper(*args, **kwargs):
return unique_everseen(func(*args, **kwargs))
return wrapper
REL = re.compile(r"""<([^>]*\srel\s{0,10}=\s{0,10}['"]?([^'" >]+)[^>]*)>""", re.I)
"""
Regex for an HTML tag with 'rel="val"' attributes.
"""
@unique_values
def find_external_links(url, page):
"""Find rel="homepage" and rel="download" links in `page`, yielding URLs"""
for match in REL.finditer(page):
tag, rel = match.groups()
rels = set(map(str.strip, rel.lower().split(',')))
if 'homepage' in rels or 'download' in rels:
for match in HREF.finditer(tag):
yield urllib.parse.urljoin(url, htmldecode(match.group(1)))
for tag in ("<th>Home Page", "<th>Download URL"):
pos = page.find(tag)
if pos != -1:
match = HREF.search(page, pos)
if match:
yield urllib.parse.urljoin(url, htmldecode(match.group(1)))
class ContentChecker:
"""
A null content checker that defines the interface for checking content
"""
def feed(self, block):
"""
Feed a block of data to the hash.
"""
return
def is_valid(self):
"""
Check the hash. Return False if validation fails.
"""
return True
def report(self, reporter, template):
"""
Call reporter with information about the checker (hash name)
substituted into the template.
"""
return
class HashChecker(ContentChecker):
pattern = re.compile(
r'(?P<hash_name>sha1|sha224|sha384|sha256|sha512|md5)='
r'(?P<expected>[a-f0-9]+)'
)
def __init__(self, hash_name, expected):
self.hash_name = hash_name
self.hash = hashlib.new(hash_name)
self.expected = expected
@classmethod
def from_url(cls, url):
"Construct a (possibly null) ContentChecker from a URL"
fragment = urllib.parse.urlparse(url)[-1]
if not fragment:
return ContentChecker()
match = cls.pattern.search(fragment)
if not match:
return ContentChecker()
return cls(**match.groupdict())
def feed(self, block):
self.hash.update(block)
def is_valid(self):
return self.hash.hexdigest() == self.expected
def report(self, reporter, template):
msg = template % self.hash_name
return reporter(msg)
class PackageIndex(Environment):
"""A distribution index that scans web pages for download URLs"""
def __init__(
self,
index_url: str = "https://pypi.org/simple/",
hosts=('*',),
ca_bundle=None,
verify_ssl: bool = True,
*args,
**kw,
):
super().__init__(*args, **kw)
self.index_url = index_url + "/"[: not index_url.endswith('/')]
self.scanned_urls: dict = {}
self.fetched_urls: dict = {}
self.package_pages: dict = {}
self.allows = re.compile('|'.join(map(translate, hosts))).match
self.to_scan: list = []
self.opener = urllib.request.urlopen
def add(self, dist):
# ignore invalid versions
try:
parse_version(dist.version)
except Exception:
return None
return super().add(dist)
# FIXME: 'PackageIndex.process_url' is too complex (14)
def process_url(self, url, retrieve: bool = False): # noqa: C901
"""Evaluate a URL as a possible download, and maybe retrieve it"""
if url in self.scanned_urls and not retrieve:
return
self.scanned_urls[url] = True
if not URL_SCHEME(url):
self.process_filename(url)
return
else:
dists = list(distros_for_url(url))
if dists:
if not self.url_ok(url):
return
self.debug("Found link: %s", url)
if dists or not retrieve or url in self.fetched_urls:
list(map(self.add, dists))
return # don't need the actual page
if not self.url_ok(url):
self.fetched_urls[url] = True
return
self.info("Reading %s", url)
self.fetched_urls[url] = True # prevent multiple fetch attempts
tmpl = "Download error on %s: %%s -- Some packages may not be found!"
f = self.open_url(url, tmpl % url)
if f is None:
return
if isinstance(f, urllib.error.HTTPError) and f.code == 401:
self.info("Authentication error: %s" % f.msg)
self.fetched_urls[f.url] = True
if 'html' not in f.headers.get('content-type', '').lower():
f.close() # not html, we can't process it
return
base = f.url # handle redirects
page = f.read()
if not isinstance(page, str):
# In Python 3 and got bytes but want str.
if isinstance(f, urllib.error.HTTPError):
# Errors have no charset, assume latin1:
charset = 'latin-1'
else:
charset = f.headers.get_param('charset') or 'latin-1'
page = page.decode(charset, "ignore")
f.close()
for match in HREF.finditer(page):
link = urllib.parse.urljoin(base, htmldecode(match.group(1)))
self.process_url(link)
if url.startswith(self.index_url) and getattr(f, 'code', None) != 404:
page = self.process_index(url, page)
def process_filename(self, fn, nested: bool = False):
# process filenames or directories
if not os.path.exists(fn):
self.warn("Not found: %s", fn)
return
if os.path.isdir(fn) and not nested:
path = os.path.realpath(fn)
for item in os.listdir(path):
self.process_filename(os.path.join(path, item), True)
dists = distros_for_filename(fn)
if dists:
self.debug("Found: %s", fn)
list(map(self.add, dists))
def url_ok(self, url, fatal: bool = False):
s = URL_SCHEME(url)
is_file = s and s.group(1).lower() == 'file'
if is_file or self.allows(urllib.parse.urlparse(url)[1]):
return True
msg = (
"\nNote: Bypassing %s (disallowed host; see "
"https://setuptools.pypa.io/en/latest/deprecated/"
"easy_install.html#restricting-downloads-with-allow-hosts for details).\n"
)
if fatal:
raise DistutilsError(msg % url)
else:
self.warn(msg, url)
return False
def scan_egg_links(self, search_path):
dirs = filter(os.path.isdir, search_path)
egg_links = (
(path, entry)
for path in dirs
for entry in os.listdir(path)
if entry.endswith('.egg-link')
)
list(itertools.starmap(self.scan_egg_link, egg_links))
def scan_egg_link(self, path, entry):
content = _read_utf8_with_fallback(os.path.join(path, entry))
# filter non-empty lines
lines = list(filter(None, map(str.strip, content.splitlines())))
if len(lines) != 2:
# format is not recognized; punt
return
egg_path, setup_path = lines
for dist in find_distributions(os.path.join(path, egg_path)):
dist.location = os.path.join(path, *lines)
dist.precedence = SOURCE_DIST
self.add(dist)
def _scan(self, link):
# Process a URL to see if it's for a package page
NO_MATCH_SENTINEL = None, None
if not link.startswith(self.index_url):
return NO_MATCH_SENTINEL
parts = list(map(urllib.parse.unquote, link[len(self.index_url) :].split('/')))
if len(parts) != 2 or '#' in parts[1]:
return NO_MATCH_SENTINEL
# it's a package page, sanitize and index it
pkg = safe_name(parts[0])
ver = safe_version(parts[1])
self.package_pages.setdefault(pkg.lower(), {})[link] = True
return to_filename(pkg), to_filename(ver)
def process_index(self, url, page):
"""Process the contents of a PyPI page"""
# process an index page into the package-page index
for match in HREF.finditer(page):
try:
self._scan(urllib.parse.urljoin(url, htmldecode(match.group(1))))
except ValueError:
pass
pkg, ver = self._scan(url) # ensure this page is in the page index
if not pkg:
return "" # no sense double-scanning non-package pages
# process individual package page
for new_url in find_external_links(url, page):
# Process the found URL
base, frag = egg_info_for_url(new_url)
if base.endswith('.py') and not frag:
if ver:
new_url += '#egg=%s-%s' % (pkg, ver)
else:
self.need_version_info(url)
self.scan_url(new_url)
return PYPI_MD5.sub(
lambda m: '<a href="%s#md5=%s">%s</a>' % m.group(1, 3, 2), page
)
def need_version_info(self, url):
self.scan_all(
"Page at %s links to .py file(s) without version info; an index "
"scan is required.",
url,
)
def scan_all(self, msg=None, *args):
if self.index_url not in self.fetched_urls:
if msg:
self.warn(msg, *args)
self.info("Scanning index of all packages (this may take a while)")
self.scan_url(self.index_url)
def find_packages(self, requirement):
self.scan_url(self.index_url + requirement.unsafe_name + '/')
if not self.package_pages.get(requirement.key):
# Fall back to safe version of the name
self.scan_url(self.index_url + requirement.project_name + '/')
if not self.package_pages.get(requirement.key):
# We couldn't find the target package, so search the index page too
self.not_found_in_index(requirement)
for url in list(self.package_pages.get(requirement.key, ())):
# scan each page that might be related to the desired package
self.scan_url(url)
def obtain(self, requirement, installer=None):
self.prescan()
self.find_packages(requirement)
for dist in self[requirement.key]:
if dist in requirement:
return dist
self.debug("%s does not match %s", requirement, dist)
return super().obtain(requirement, installer)
def check_hash(self, checker, filename, tfp):
"""
checker is a ContentChecker
"""
checker.report(self.debug, "Validating %%s checksum for %s" % filename)
if not checker.is_valid():
tfp.close()
os.unlink(filename)
raise DistutilsError(
"%s validation failed for %s; "
"possible download problem?"
% (checker.hash.name, os.path.basename(filename))
)
def add_find_links(self, urls):
"""Add `urls` to the list that will be prescanned for searches"""
for url in urls:
if (
self.to_scan is None # if we have already "gone online"
or not URL_SCHEME(url) # or it's a local file/directory
or url.startswith('file:')
or list(distros_for_url(url)) # or a direct package link
):
# then go ahead and process it now
self.scan_url(url)
else:
# otherwise, defer retrieval till later
self.to_scan.append(url)
def prescan(self):
"""Scan urls scheduled for prescanning (e.g. --find-links)"""
if self.to_scan:
list(map(self.scan_url, self.to_scan))
self.to_scan = None # from now on, go ahead and process immediately
def not_found_in_index(self, requirement):
if self[requirement.key]: # we've seen at least one distro
meth, msg = self.info, "Couldn't retrieve index page for %r"
else: # no distros seen for this name, might be misspelled
meth, msg = self.warn, "Couldn't find index page for %r (maybe misspelled?)"
meth(msg, requirement.unsafe_name)
self.scan_all()
def download(self, spec, tmpdir):
"""Locate and/or download `spec` to `tmpdir`, returning a local path
`spec` may be a ``Requirement`` object, or a string containing a URL,
an existing local filename, or a project/version requirement spec
(i.e. the string form of a ``Requirement`` object). If it is the URL
of a .py file with an unambiguous ``#egg=name-version`` tag (i.e., one
that escapes ``-`` as ``_`` throughout), a trivial ``setup.py`` is
automatically created alongside the downloaded file.
If `spec` is a ``Requirement`` object or a string containing a
project/version requirement spec, this method returns the location of
a matching distribution (possibly after downloading it to `tmpdir`).
If `spec` is a locally existing file or directory name, it is simply
returned unchanged. If `spec` is a URL, it is downloaded to a subpath
of `tmpdir`, and the local filename is returned. Various errors may be
raised if a problem occurs during downloading.
"""
if not isinstance(spec, Requirement):
scheme = URL_SCHEME(spec)
if scheme:
# It's a url, download it to tmpdir
found = self._download_url(spec, tmpdir)
base, fragment = egg_info_for_url(spec)
if base.endswith('.py'):
found = self.gen_setup(found, fragment, tmpdir)
return found
elif os.path.exists(spec):
# Existing file or directory, just return it
return spec
else:
spec = parse_requirement_arg(spec)
return getattr(self.fetch_distribution(spec, tmpdir), 'location', None)
def fetch_distribution( # noqa: C901 # is too complex (14) # FIXME
self,
requirement,
tmpdir,
force_scan: bool = False,
source: bool = False,
develop_ok: bool = False,
local_index=None,
):
"""Obtain a distribution suitable for fulfilling `requirement`
`requirement` must be a ``pkg_resources.Requirement`` instance.
If necessary, or if the `force_scan` flag is set, the requirement is
searched for in the (online) package index as well as the locally
installed packages. If a distribution matching `requirement` is found,
the returned distribution's ``location`` is the value you would have
gotten from calling the ``download()`` method with the matching
distribution's URL or filename. If no matching distribution is found,
``None`` is returned.
If the `source` flag is set, only source distributions and source
checkout links will be considered. Unless the `develop_ok` flag is
set, development and system eggs (i.e., those using the ``.egg-info``
format) will be ignored.
"""
# process a Requirement
self.info("Searching for %s", requirement)
skipped = set()
dist = None
def find(req, env=None):
if env is None:
env = self
# Find a matching distribution; may be called more than once
for dist in env[req.key]:
if dist.precedence == DEVELOP_DIST and not develop_ok:
if dist not in skipped:
self.warn(
"Skipping development or system egg: %s",
dist,
)
skipped.add(dist)
continue
test = dist in req and (dist.precedence <= SOURCE_DIST or not source)
if test:
loc = self.download(dist.location, tmpdir)
dist.download_location = loc
if os.path.exists(dist.download_location):
return dist
return None
if force_scan:
self.prescan()
self.find_packages(requirement)
dist = find(requirement)
if not dist and local_index is not None:
dist = find(requirement, local_index)
if dist is None:
if self.to_scan is not None:
self.prescan()
dist = find(requirement)
if dist is None and not force_scan:
self.find_packages(requirement)
dist = find(requirement)
if dist is None:
self.warn(
"No local packages or working download links found for %s%s",
(source and "a source distribution of " or ""),
requirement,
)
return None
else:
self.info("Best match: %s", dist)
return dist.clone(location=dist.download_location)
def fetch(
self, requirement, tmpdir, force_scan: bool = False, source: bool = False
):
"""Obtain a file suitable for fulfilling `requirement`
DEPRECATED; use the ``fetch_distribution()`` method now instead. For
backward compatibility, this routine is identical but returns the
``location`` of the downloaded distribution instead of a distribution
object.
"""
dist = self.fetch_distribution(requirement, tmpdir, force_scan, source)
if dist is not None:
return dist.location
return None
def gen_setup(self, filename, fragment, tmpdir):
match = EGG_FRAGMENT.match(fragment)
dists = (
match
and [
d
for d in interpret_distro_name(filename, match.group(1), None)
if d.version
]
or []
)
if len(dists) == 1: # unambiguous ``#egg`` fragment
basename = os.path.basename(filename)
# Make sure the file has been downloaded to the temp dir.
if os.path.dirname(filename) != tmpdir:
dst = os.path.join(tmpdir, basename)
if not (os.path.exists(dst) and os.path.samefile(filename, dst)):
shutil.copy2(filename, dst)
filename = dst
with open(os.path.join(tmpdir, 'setup.py'), 'w', encoding="utf-8") as file:
file.write(
"from setuptools import setup\n"
"setup(name=%r, version=%r, py_modules=[%r])\n"
% (
dists[0].project_name,
dists[0].version,
os.path.splitext(basename)[0],
)
)
return filename
elif match:
raise DistutilsError(
"Can't unambiguously interpret project/version identifier %r; "
"any dashes in the name or version should be escaped using "
"underscores. %r" % (fragment, dists)
)
else:
raise DistutilsError(
"Can't process plain .py files without an '#egg=name-version'"
" suffix to enable automatic setup script generation."
)
dl_blocksize = 8192
def _download_to(self, url, filename):
self.info("Downloading %s", url)
# Download the file
fp = None
try:
checker = HashChecker.from_url(url)
fp = self.open_url(url)
if isinstance(fp, urllib.error.HTTPError):
raise DistutilsError(
"Can't download %s: %s %s" % (url, fp.code, fp.msg)
)
headers = fp.info()
blocknum = 0
bs = self.dl_blocksize
size = -1
if "content-length" in headers:
# Some servers return multiple Content-Length headers :(
sizes = headers.get_all('Content-Length')
size = max(map(int, sizes))
self.reporthook(url, filename, blocknum, bs, size)
with open(filename, 'wb') as tfp:
while True:
block = fp.read(bs)
if block:
checker.feed(block)
tfp.write(block)
blocknum += 1
self.reporthook(url, filename, blocknum, bs, size)
else:
break
self.check_hash(checker, filename, tfp)
return headers
finally:
if fp:
fp.close()
def reporthook(self, url, filename, blocknum, blksize, size):
pass # no-op
# FIXME:
def open_url(self, url, warning=None): # noqa: C901 # is too complex (12)
if url.startswith('file:'):
return local_open(url)
try:
return open_with_auth(url, self.opener)
except (ValueError, http.client.InvalidURL) as v:
msg = ' '.join([str(arg) for arg in v.args])
if warning:
self.warn(warning, msg)
else:
raise DistutilsError('%s %s' % (url, msg)) from v
except urllib.error.HTTPError as v:
return v
except urllib.error.URLError as v:
if warning:
self.warn(warning, v.reason)
else:
raise DistutilsError(
"Download error for %s: %s" % (url, v.reason)
) from v
except http.client.BadStatusLine as v:
if warning:
self.warn(warning, v.line)
else:
raise DistutilsError(
'%s returned a bad status line. The server might be '
'down, %s' % (url, v.line)
) from v
except (http.client.HTTPException, OSError) as v:
if warning:
self.warn(warning, v)
else:
raise DistutilsError("Download error for %s: %s" % (url, v)) from v
@staticmethod
def _sanitize(name):
r"""
Replace unsafe path directives with underscores.
>>> san = PackageIndex._sanitize
>>> san('/home/user/.ssh/authorized_keys')
'_home_user_.ssh_authorized_keys'
>>> san('..\\foo\\bing')
'__foo_bing'
>>> san('D:bar')
'D_bar'
>>> san('C:\\bar')
'C__bar'
>>> san('foo..bar')
'foo..bar'
>>> san('D:../foo')
'D___foo'
"""
pattern = '|'.join((
# drive letters
r':',
# path separators
r'[/\\]',
# parent dirs
r'(?:(?<=([/\\]|:))\.\.(?=[/\\]|$))|(?:^\.\.(?=[/\\]|$))',
))
return re.sub(pattern, r'_', name)
@classmethod
def _resolve_download_filename(cls, url, tmpdir):
"""
>>> import pathlib
>>> du = PackageIndex._resolve_download_filename
>>> root = getfixture('tmp_path')
>>> url = 'https://files.pythonhosted.org/packages/a9/5a/0db.../setuptools-78.1.0.tar.gz'
>>> str(pathlib.Path(du(url, root)).relative_to(root))
'setuptools-78.1.0.tar.gz'
"""
name, _fragment = egg_info_for_url(url)
name = cls._sanitize(
name
or
# default if URL has no path contents
'__downloaded__'
)
# strip any extra .zip before download
name = re.sub(r'\.egg\.zip$', '.egg', name)
return os.path.join(tmpdir, name)
def _download_url(self, url, tmpdir):
"""
Determine the download filename.
"""
filename = self._resolve_download_filename(url, tmpdir)
return self._download_vcs(url, filename) or self._download_other(url, filename)
@staticmethod
def _resolve_vcs(url):
"""
>>> rvcs = PackageIndex._resolve_vcs
>>> rvcs('git+http://foo/bar')
'git'
>>> rvcs('hg+https://foo/bar')
'hg'
>>> rvcs('git:myhost')
'git'
>>> rvcs('hg:myhost')
>>> rvcs('http://foo/bar')
"""
scheme = urllib.parse.urlsplit(url).scheme
pre, sep, post = scheme.partition('+')
# svn and git have their own protocol; hg does not
allowed = set(['svn', 'git'] + ['hg'] * bool(sep))
return next(iter({pre} & allowed), None)
def _download_vcs(self, url, spec_filename):
vcs = self._resolve_vcs(url)
if not vcs:
return None
if vcs == 'svn':
raise DistutilsError(
f"Invalid config, SVN download is not supported: {url}"
)
filename, _, _ = spec_filename.partition('#')
url, rev = self._vcs_split_rev_from_url(url)
self.info(f"Doing {vcs} clone from {url} to {filename}")
subprocess.check_call([vcs, 'clone', '--quiet', url, filename])
co_commands = dict(
git=[vcs, '-C', filename, 'checkout', '--quiet', rev],
hg=[vcs, '--cwd', filename, 'up', '-C', '-r', rev, '-q'],
)
if rev is not None:
self.info(f"Checking out {rev}")
subprocess.check_call(co_commands[vcs])
return filename
def _download_other(self, url, filename):
scheme = urllib.parse.urlsplit(url).scheme
if scheme == 'file': # pragma: no cover
return urllib.request.url2pathname(urllib.parse.urlparse(url).path)
# raise error if not allowed
self.url_ok(url, True)
return self._attempt_download(url, filename)
def scan_url(self, url):
self.process_url(url, True)
def _attempt_download(self, url, filename):
headers = self._download_to(url, filename)
if 'html' in headers.get('content-type', '').lower():
return self._invalid_download_html(url, headers, filename)
else:
return filename
def _invalid_download_html(self, url, headers, filename):
os.unlink(filename)
raise DistutilsError(f"Unexpected HTML page found at {url}")
@staticmethod
def _vcs_split_rev_from_url(url):
"""
Given a possible VCS URL, return a clean URL and resolved revision if any.
>>> vsrfu = PackageIndex._vcs_split_rev_from_url
>>> vsrfu('git+https://github.com/pypa/setuptools@v69.0.0#egg-info=setuptools')
('https://github.com/pypa/setuptools', 'v69.0.0')
>>> vsrfu('git+https://github.com/pypa/setuptools#egg-info=setuptools')
('https://github.com/pypa/setuptools', None)
>>> vsrfu('http://foo/bar')
('http://foo/bar', None)
"""
parts = urllib.parse.urlsplit(url)
clean_scheme = parts.scheme.split('+', 1)[-1]
# Some fragment identification fails
no_fragment_path, _, _ = parts.path.partition('#')
pre, sep, post = no_fragment_path.rpartition('@')
clean_path, rev = (pre, post) if sep else (post, None)
resolved = parts._replace(
scheme=clean_scheme,
path=clean_path,
# discard the fragment
fragment='',
).geturl()
return resolved, rev
def debug(self, msg, *args):
log.debug(msg, *args)
def info(self, msg, *args):
log.info(msg, *args)
def warn(self, msg, *args):
log.warn(msg, *args)
# This pattern matches a character entity reference (a decimal numeric
# references, a hexadecimal numeric reference, or a named reference).
entity_sub = re.compile(r'&(#(\d+|x[\da-fA-F]+)|[\w.:-]+);?').sub
def decode_entity(match):
what = match.group(0)
return html.unescape(what)
def htmldecode(text):
"""
Decode HTML entities in the given text.
>>> htmldecode(
... 'https://../package_name-0.1.2.tar.gz'
... '?tokena=A&amp;tokenb=B">package_name-0.1.2.tar.gz')
'https://../package_name-0.1.2.tar.gz?tokena=A&tokenb=B">package_name-0.1.2.tar.gz'
"""
return entity_sub(decode_entity, text)
def socket_timeout(timeout=15):
def _socket_timeout(func):
def _socket_timeout(*args, **kwargs):
old_timeout = socket.getdefaulttimeout()
socket.setdefaulttimeout(timeout)
try:
return func(*args, **kwargs)
finally:
socket.setdefaulttimeout(old_timeout)
return _socket_timeout
return _socket_timeout
def _encode_auth(auth):
"""
Encode auth from a URL suitable for an HTTP header.
>>> str(_encode_auth('username%3Apassword'))
'dXNlcm5hbWU6cGFzc3dvcmQ='
Long auth strings should not cause a newline to be inserted.
>>> long_auth = 'username:' + 'password'*10
>>> chr(10) in str(_encode_auth(long_auth))
False
"""
auth_s = urllib.parse.unquote(auth)
# convert to bytes
auth_bytes = auth_s.encode()
encoded_bytes = base64.b64encode(auth_bytes)
# convert back to a string
encoded = encoded_bytes.decode()
# strip the trailing carriage return
return encoded.replace('\n', '')
class Credential(NamedTuple):
"""
A username/password pair.
Displayed separated by `:`.
>>> str(Credential('username', 'password'))
'username:password'
"""
username: str
password: str
def __str__(self) -> str:
return f'{self.username}:{self.password}'
class PyPIConfig(configparser.RawConfigParser):
def __init__(self):
"""
Load from ~/.pypirc
"""
defaults = dict.fromkeys(['username', 'password', 'repository'], '')
super().__init__(defaults)
rc = os.path.join(os.path.expanduser('~'), '.pypirc')
if os.path.exists(rc):
_cfg_read_utf8_with_fallback(self, rc)
@property
def creds_by_repository(self):
sections_with_repositories = [
section
for section in self.sections()
if self.get(section, 'repository').strip()
]
return dict(map(self._get_repo_cred, sections_with_repositories))
def _get_repo_cred(self, section):
repo = self.get(section, 'repository').strip()
return repo, Credential(
self.get(section, 'username').strip(),
self.get(section, 'password').strip(),
)
def find_credential(self, url):
"""
If the URL indicated appears to be a repository defined in this
config, return the credential for that repository.
"""
for repository, cred in self.creds_by_repository.items():
if url.startswith(repository):
return cred
return None
def open_with_auth(url, opener=urllib.request.urlopen):
"""Open a urllib2 request, handling HTTP authentication"""
parsed = urllib.parse.urlparse(url)
scheme, netloc, path, params, query, frag = parsed
# Double scheme does not raise on macOS as revealed by a
# failing test. We would expect "nonnumeric port". Refs #20.
if netloc.endswith(':'):
raise http.client.InvalidURL("nonnumeric port: ''")
if scheme in ('http', 'https'):
auth, address = _splituser(netloc)
else:
auth, address = (None, None)
if not auth:
cred = PyPIConfig().find_credential(url)
if cred:
auth = str(cred)
info = cred.username, url
log.info('Authenticating as %s for %s (from .pypirc)', *info)
if auth:
auth = "Basic " + _encode_auth(auth)
parts = scheme, address, path, params, query, frag
new_url = urllib.parse.urlunparse(parts)
request = urllib.request.Request(new_url)
request.add_header("Authorization", auth)
else:
request = urllib.request.Request(url)
request.add_header('User-Agent', user_agent)
fp = opener(request)
if auth:
# Put authentication info back into request URL if same host,
# so that links found on the page will work
s2, h2, path2, param2, query2, frag2 = urllib.parse.urlparse(fp.url)
if s2 == scheme and h2 == address:
parts = s2, netloc, path2, param2, query2, frag2
fp.url = urllib.parse.urlunparse(parts)
return fp
# copy of urllib.parse._splituser from Python 3.8
def _splituser(host):
"""splituser('user[:passwd]@host[:port]')
--> 'user[:passwd]', 'host[:port]'."""
user, delim, host = host.rpartition('@')
return (user if delim else None), host
# adding a timeout to avoid freezing package_index
open_with_auth = socket_timeout(_SOCKET_TIMEOUT)(open_with_auth)
def fix_sf_url(url):
return url # backward compatibility
def local_open(url):
"""Read a local path, with special support for directories"""
scheme, server, path, param, query, frag = urllib.parse.urlparse(url)
filename = urllib.request.url2pathname(path)
if os.path.isfile(filename):
return urllib.request.urlopen(url)
elif path.endswith('/') and os.path.isdir(filename):
files = []
for f in os.listdir(filename):
filepath = os.path.join(filename, f)
if f == 'index.html':
body = _read_utf8_with_fallback(filepath)
break
elif os.path.isdir(filepath):
f += '/'
files.append('<a href="{name}">{name}</a>'.format(name=f))
else:
tmpl = "<html><head><title>{url}</title></head><body>{files}</body></html>"
body = tmpl.format(url=url, files='\n'.join(files))
status, message = 200, "OK"
else:
status, message, body = 404, "Path not found", "Not found"
headers = {'content-type': 'text/html'}
body_stream = io.StringIO(body)
return urllib.error.HTTPError(url, status, message, headers, body_stream)
from __future__ import annotations
import builtins
import contextlib
import functools
import itertools
import operator
import os
import pickle
import re
import sys
import tempfile
import textwrap
from types import TracebackType
from typing import TYPE_CHECKING
import pkg_resources
from pkg_resources import working_set
from distutils.errors import DistutilsError
if sys.platform.startswith('java'):
import org.python.modules.posix.PosixModule as _os # pyright: ignore[reportMissingImports]
else:
_os = sys.modules[os.name]
_open = open
if TYPE_CHECKING:
from typing_extensions import Self
__all__ = [
"AbstractSandbox",
"DirectorySandbox",
"SandboxViolation",
"run_setup",
]
def _execfile(filename, globals, locals=None):
"""
Python 3 implementation of execfile.
"""
mode = 'rb'
with open(filename, mode) as stream:
script = stream.read()
if locals is None:
locals = globals
code = compile(script, filename, 'exec')
exec(code, globals, locals)
@contextlib.contextmanager
def save_argv(repl=None):
saved = sys.argv[:]
if repl is not None:
sys.argv[:] = repl
try:
yield saved
finally:
sys.argv[:] = saved
@contextlib.contextmanager
def save_path():
saved = sys.path[:]
try:
yield saved
finally:
sys.path[:] = saved
@contextlib.contextmanager
def override_temp(replacement):
"""
Monkey-patch tempfile.tempdir with replacement, ensuring it exists
"""
os.makedirs(replacement, exist_ok=True)
saved = tempfile.tempdir
tempfile.tempdir = replacement
try:
yield
finally:
tempfile.tempdir = saved
@contextlib.contextmanager
def pushd(target):
saved = os.getcwd()
os.chdir(target)
try:
yield saved
finally:
os.chdir(saved)
class UnpickleableException(Exception):
"""
An exception representing another Exception that could not be pickled.
"""
@staticmethod
def dump(type, exc):
"""
Always return a dumped (pickled) type and exc. If exc can't be pickled,
wrap it in UnpickleableException first.
"""
try:
return pickle.dumps(type), pickle.dumps(exc)
except Exception:
# get UnpickleableException inside the sandbox
from setuptools.sandbox import UnpickleableException as cls
return cls.dump(cls, cls(repr(exc)))
class ExceptionSaver:
"""
A Context Manager that will save an exception, serialize, and restore it
later.
"""
def __enter__(self) -> Self:
return self
def __exit__(
self,
type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> bool:
if not exc:
return False
# dump the exception
self._saved = UnpickleableException.dump(type, exc)
self._tb = tb
# suppress the exception
return True
def resume(self):
"restore and re-raise any exception"
if '_saved' not in vars(self):
return
type, exc = map(pickle.loads, self._saved)
raise exc.with_traceback(self._tb)
@contextlib.contextmanager
def save_modules():
"""
Context in which imported modules are saved.
Translates exceptions internal to the context into the equivalent exception
outside the context.
"""
saved = sys.modules.copy()
with ExceptionSaver() as saved_exc:
yield saved
sys.modules.update(saved)
# remove any modules imported since
del_modules = (
mod_name
for mod_name in sys.modules
if mod_name not in saved
# exclude any encodings modules. See #285
and not mod_name.startswith('encodings.')
)
_clear_modules(del_modules)
saved_exc.resume()
def _clear_modules(module_names):
for mod_name in list(module_names):
del sys.modules[mod_name]
@contextlib.contextmanager
def save_pkg_resources_state():
saved = pkg_resources.__getstate__()
try:
yield saved
finally:
pkg_resources.__setstate__(saved)
@contextlib.contextmanager
def setup_context(setup_dir):
temp_dir = os.path.join(setup_dir, 'temp')
with save_pkg_resources_state():
with save_modules():
with save_path():
hide_setuptools()
with save_argv():
with override_temp(temp_dir):
with pushd(setup_dir):
# ensure setuptools commands are available
__import__('setuptools')
yield
_MODULES_TO_HIDE = {
'setuptools',
'distutils',
'pkg_resources',
'Cython',
'_distutils_hack',
}
def _needs_hiding(mod_name):
"""
>>> _needs_hiding('setuptools')
True
>>> _needs_hiding('pkg_resources')
True
>>> _needs_hiding('setuptools_plugin')
False
>>> _needs_hiding('setuptools.__init__')
True
>>> _needs_hiding('distutils')
True
>>> _needs_hiding('os')
False
>>> _needs_hiding('Cython')
True
"""
base_module = mod_name.split('.', 1)[0]
return base_module in _MODULES_TO_HIDE
def hide_setuptools():
"""
Remove references to setuptools' modules from sys.modules to allow the
invocation to import the most appropriate setuptools. This technique is
necessary to avoid issues such as #315 where setuptools upgrading itself
would fail to find a function declared in the metadata.
"""
_distutils_hack = sys.modules.get('_distutils_hack', None)
if _distutils_hack is not None:
_distutils_hack._remove_shim()
modules = filter(_needs_hiding, sys.modules)
_clear_modules(modules)
def run_setup(setup_script, args):
"""Run a distutils setup script, sandboxed in its directory"""
setup_dir = os.path.abspath(os.path.dirname(setup_script))
with setup_context(setup_dir):
try:
sys.argv[:] = [setup_script] + list(args)
sys.path.insert(0, setup_dir)
# reset to include setup dir, w/clean callback list
working_set.__init__()
working_set.callbacks.append(lambda dist: dist.activate())
with DirectorySandbox(setup_dir):
ns = dict(__file__=setup_script, __name__='__main__')
_execfile(setup_script, ns)
except SystemExit as v:
if v.args and v.args[0]:
raise
# Normal exit, just return
class AbstractSandbox:
"""Wrap 'os' module and 'open()' builtin for virtualizing setup scripts"""
_active = False
def __init__(self):
self._attrs = [
name
for name in dir(_os)
if not name.startswith('_') and hasattr(self, name)
]
def _copy(self, source):
for name in self._attrs:
setattr(os, name, getattr(source, name))
def __enter__(self) -> None:
self._copy(self)
builtins.open = self._open
self._active = True
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
):
self._active = False
builtins.open = _open
self._copy(_os)
def run(self, func):
"""Run 'func' under os sandboxing"""
with self:
return func()
def _mk_dual_path_wrapper(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099
original = getattr(_os, name)
def wrap(self, src, dst, *args, **kw):
if self._active:
src, dst = self._remap_pair(name, src, dst, *args, **kw)
return original(src, dst, *args, **kw)
return wrap
for __name in ["rename", "link", "symlink"]:
if hasattr(_os, __name):
locals()[__name] = _mk_dual_path_wrapper(__name)
def _mk_single_path_wrapper(name: str, original=None): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099
original = original or getattr(_os, name)
def wrap(self, path, *args, **kw):
if self._active:
path = self._remap_input(name, path, *args, **kw)
return original(path, *args, **kw)
return wrap
_open = _mk_single_path_wrapper('open', _open)
for __name in [
"stat",
"listdir",
"chdir",
"open",
"chmod",
"chown",
"mkdir",
"remove",
"unlink",
"rmdir",
"utime",
"lchown",
"chroot",
"lstat",
"startfile",
"mkfifo",
"mknod",
"pathconf",
"access",
]:
if hasattr(_os, __name):
locals()[__name] = _mk_single_path_wrapper(__name)
def _mk_single_with_return(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099
original = getattr(_os, name)
def wrap(self, path, *args, **kw):
if self._active:
path = self._remap_input(name, path, *args, **kw)
return self._remap_output(name, original(path, *args, **kw))
return original(path, *args, **kw)
return wrap
for __name in ['readlink', 'tempnam']:
if hasattr(_os, __name):
locals()[__name] = _mk_single_with_return(__name)
def _mk_query(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099
original = getattr(_os, name)
def wrap(self, *args, **kw):
retval = original(*args, **kw)
if self._active:
return self._remap_output(name, retval)
return retval
return wrap
for __name in ['getcwd', 'tmpnam']:
if hasattr(_os, __name):
locals()[__name] = _mk_query(__name)
def _validate_path(self, path):
"""Called to remap or validate any path, whether input or output"""
return path
def _remap_input(self, operation, path, *args, **kw):
"""Called for path inputs"""
return self._validate_path(path)
def _remap_output(self, operation, path):
"""Called for path outputs"""
return self._validate_path(path)
def _remap_pair(self, operation, src, dst, *args, **kw):
"""Called for path pairs like rename, link, and symlink operations"""
return (
self._remap_input(operation + '-from', src, *args, **kw),
self._remap_input(operation + '-to', dst, *args, **kw),
)
if hasattr(os, 'devnull'):
_EXCEPTIONS = [os.devnull]
else:
_EXCEPTIONS = []
class DirectorySandbox(AbstractSandbox):
"""Restrict operations to a single subdirectory - pseudo-chroot"""
write_ops: dict[str, None] = dict.fromkeys([
"open",
"chmod",
"chown",
"mkdir",
"remove",
"unlink",
"rmdir",
"utime",
"lchown",
"chroot",
"mkfifo",
"mknod",
"tempnam",
])
_exception_patterns: list[str | re.Pattern] = []
"exempt writing to paths that match the pattern"
def __init__(self, sandbox, exceptions=_EXCEPTIONS):
self._sandbox = os.path.normcase(os.path.realpath(sandbox))
self._prefix = os.path.join(self._sandbox, '')
self._exceptions = [
os.path.normcase(os.path.realpath(path)) for path in exceptions
]
AbstractSandbox.__init__(self)
def _violation(self, operation, *args, **kw):
from setuptools.sandbox import SandboxViolation
raise SandboxViolation(operation, args, kw)
def _open(self, path, mode='r', *args, **kw):
if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path):
self._violation("open", path, mode, *args, **kw)
return _open(path, mode, *args, **kw)
def tmpnam(self):
self._violation("tmpnam")
def _ok(self, path):
active = self._active
try:
self._active = False
realpath = os.path.normcase(os.path.realpath(path))
return (
self._exempted(realpath)
or realpath == self._sandbox
or realpath.startswith(self._prefix)
)
finally:
self._active = active
def _exempted(self, filepath):
start_matches = (
filepath.startswith(exception) for exception in self._exceptions
)
pattern_matches = (
re.match(pattern, filepath) for pattern in self._exception_patterns
)
candidates = itertools.chain(start_matches, pattern_matches)
return any(candidates)
def _remap_input(self, operation, path, *args, **kw):
"""Called for path inputs"""
if operation in self.write_ops and not self._ok(path):
self._violation(operation, os.path.realpath(path), *args, **kw)
return path
def _remap_pair(self, operation, src, dst, *args, **kw):
"""Called for path pairs like rename, link, and symlink operations"""
if not self._ok(src) or not self._ok(dst):
self._violation(operation, src, dst, *args, **kw)
return (src, dst)
def open(self, file, flags, mode: int = 0o777, *args, **kw):
"""Called for low-level os.open()"""
if flags & WRITE_FLAGS and not self._ok(file):
self._violation("os.open", file, flags, mode, *args, **kw)
return _os.open(file, flags, mode, *args, **kw)
WRITE_FLAGS = functools.reduce(
operator.or_,
[
getattr(_os, a, 0)
for a in "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split()
],
)
class SandboxViolation(DistutilsError):
"""A setup script attempted to modify the filesystem outside the sandbox"""
tmpl = textwrap.dedent(
"""
SandboxViolation: {cmd}{args!r} {kwargs}
The package setup script has attempted to modify files on your system
that are not within the EasyInstall build area, and has been aborted.
This package cannot be safely installed by EasyInstall, and may not
support alternate installation locations even if you run its setup
script by hand. Please inform the package's author and the EasyInstall
maintainers to find out if a fix or workaround is available.
"""
).lstrip()
def __str__(self) -> str:
cmd, args, kwargs = self.args
return self.tmpl.format(**locals())
"""Basic http server for tests to simulate PyPI or custom indexes"""
import http.server
import os
import threading
import time
import urllib.parse
import urllib.request
class IndexServer(http.server.HTTPServer):
"""Basic single-threaded http server simulating a package index
You can use this server in unittest like this::
s = IndexServer()
s.start()
index_url = s.base_url() + 'mytestindex'
# do some test requests to the index
# The index files should be located in setuptools/tests/indexes
s.stop()
"""
def __init__(
self,
server_address=('', 0),
RequestHandlerClass=http.server.SimpleHTTPRequestHandler,
):
http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
self._run = True
def start(self):
self.thread = threading.Thread(target=self.serve_forever)
self.thread.start()
def stop(self):
"Stop the server"
# Let the server finish the last request and wait for a new one.
time.sleep(0.1)
self.shutdown()
self.thread.join()
self.socket.close()
def base_url(self):
port = self.server_port
return 'http://127.0.0.1:%s/setuptools/tests/indexes/' % port
class RequestRecorder(http.server.BaseHTTPRequestHandler):
def do_GET(self):
requests = vars(self.server).setdefault('requests', [])
requests.append(self)
self.send_response(200, 'OK')
class MockServer(http.server.HTTPServer, threading.Thread):
"""
A simple HTTP Server that records the requests made to it.
"""
def __init__(self, server_address=('', 0), RequestHandlerClass=RequestRecorder):
http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
threading.Thread.__init__(self)
self.daemon = True
self.requests = []
def run(self):
self.serve_forever()
@property
def netloc(self):
return 'localhost:%s' % self.server_port
@property
def url(self):
return 'http://%s/' % self.netloc
def path_to_url(path, authority=None):
"""Convert a path to a file: URL."""
path = os.path.normpath(os.path.abspath(path))
base = 'file:'
if authority is not None:
base += '//' + authority
return urllib.parse.urljoin(base, urllib.request.pathname2url(path))
"""Easy install Tests"""
import contextlib
import io
import itertools
import logging
import os
import pathlib
import re
import site
import subprocess
import sys
import tarfile
import tempfile
import time
import warnings
import zipfile
from pathlib import Path
from typing import NamedTuple
from unittest import mock
import pytest
from jaraco import path
import pkg_resources
import setuptools.command.easy_install as ei
from pkg_resources import Distribution as PRDistribution, normalize_path, working_set
from setuptools import sandbox
from setuptools._normalization import safer_name
from setuptools.command.easy_install import PthDistributions
from setuptools.dist import Distribution
from setuptools.sandbox import run_setup
from setuptools.tests import fail_on_ascii
from setuptools.tests.server import MockServer, path_to_url
from . import contexts
from .textwrap import DALS
import distutils.errors
@pytest.fixture(autouse=True)
def pip_disable_index(monkeypatch):
"""
Important: Disable the default index for pip to avoid
querying packages in the index and potentially resolving
and installing packages there.
"""
monkeypatch.setenv('PIP_NO_INDEX', 'true')
class FakeDist:
def get_entry_map(self, group):
if group != 'console_scripts':
return {}
return {'name': 'ep'}
def as_requirement(self):
return 'spec'
SETUP_PY = DALS(
"""
from setuptools import setup
setup()
"""
)
class TestEasyInstallTest:
def test_get_script_args(self):
header = ei.CommandSpec.best().from_environment().as_header()
dist = FakeDist()
args = next(ei.ScriptWriter.get_args(dist))
name, script = itertools.islice(args, 2)
assert script.startswith(header)
assert "'spec'" in script
assert "'console_scripts'" in script
assert "'name'" in script
assert re.search('^# EASY-INSTALL-ENTRY-SCRIPT', script, flags=re.MULTILINE)
def test_no_find_links(self):
# new option '--no-find-links', that blocks find-links added at
# the project level
dist = Distribution()
cmd = ei.easy_install(dist)
cmd.check_pth_processing = lambda: True
cmd.no_find_links = True
cmd.find_links = ['link1', 'link2']
cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok')
cmd.args = ['ok']
cmd.ensure_finalized()
assert cmd.package_index.scanned_urls == {}
# let's try without it (default behavior)
cmd = ei.easy_install(dist)
cmd.check_pth_processing = lambda: True
cmd.find_links = ['link1', 'link2']
cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok')
cmd.args = ['ok']
cmd.ensure_finalized()
keys = sorted(cmd.package_index.scanned_urls.keys())
assert keys == ['link1', 'link2']
def test_write_exception(self):
"""
Test that `cant_write_to_target` is rendered as a DistutilsError.
"""
dist = Distribution()
cmd = ei.easy_install(dist)
cmd.install_dir = os.getcwd()
with pytest.raises(distutils.errors.DistutilsError):
cmd.cant_write_to_target()
def test_all_site_dirs(self, monkeypatch):
"""
get_site_dirs should always return site dirs reported by
site.getsitepackages.
"""
path = normalize_path('/setuptools/test/site-packages')
def mock_gsp():
return [path]
monkeypatch.setattr(site, 'getsitepackages', mock_gsp, raising=False)
assert path in ei.get_site_dirs()
def test_all_site_dirs_works_without_getsitepackages(self, monkeypatch):
monkeypatch.delattr(site, 'getsitepackages', raising=False)
assert ei.get_site_dirs()
@pytest.fixture
def sdist_unicode(self, tmpdir):
files = [
(
'setup.py',
DALS(
"""
import setuptools
setuptools.setup(
name="setuptools-test-unicode",
version="1.0",
packages=["mypkg"],
include_package_data=True,
)
"""
),
),
(
'mypkg/__init__.py',
"",
),
(
'mypkg/☃.txt',
"",
),
]
sdist_name = 'setuptools-test-unicode-1.0.zip'
sdist = tmpdir / sdist_name
# can't use make_sdist, because the issue only occurs
# with zip sdists.
sdist_zip = zipfile.ZipFile(str(sdist), 'w')
for filename, content in files:
sdist_zip.writestr(filename, content)
sdist_zip.close()
return str(sdist)
@fail_on_ascii
def test_unicode_filename_in_sdist(self, sdist_unicode, tmpdir, monkeypatch):
"""
The install command should execute correctly even if
the package has unicode filenames.
"""
dist = Distribution({'script_args': ['easy_install']})
target = (tmpdir / 'target').ensure_dir()
cmd = ei.easy_install(
dist,
install_dir=str(target),
args=['x'],
)
monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target))
cmd.ensure_finalized()
cmd.easy_install(sdist_unicode)
@pytest.fixture
def sdist_unicode_in_script(self, tmpdir):
files = [
(
"setup.py",
DALS(
"""
import setuptools
setuptools.setup(
name="setuptools-test-unicode",
version="1.0",
packages=["mypkg"],
include_package_data=True,
scripts=['mypkg/unicode_in_script'],
)
"""
),
),
("mypkg/__init__.py", ""),
(
"mypkg/unicode_in_script",
DALS(
"""
#!/bin/sh
# á
non_python_fn() {
}
"""
),
),
]
sdist_name = "setuptools-test-unicode-script-1.0.zip"
sdist = tmpdir / sdist_name
# can't use make_sdist, because the issue only occurs
# with zip sdists.
sdist_zip = zipfile.ZipFile(str(sdist), "w")
for filename, content in files:
sdist_zip.writestr(filename, content.encode('utf-8'))
sdist_zip.close()
return str(sdist)
@fail_on_ascii
def test_unicode_content_in_sdist(
self, sdist_unicode_in_script, tmpdir, monkeypatch
):
"""
The install command should execute correctly even if
the package has unicode in scripts.
"""
dist = Distribution({"script_args": ["easy_install"]})
target = (tmpdir / "target").ensure_dir()
cmd = ei.easy_install(dist, install_dir=str(target), args=["x"])
monkeypatch.setitem(os.environ, "PYTHONPATH", str(target))
cmd.ensure_finalized()
cmd.easy_install(sdist_unicode_in_script)
@pytest.fixture
def sdist_script(self, tmpdir):
files = [
(
'setup.py',
DALS(
"""
import setuptools
setuptools.setup(
name="setuptools-test-script",
version="1.0",
scripts=["mypkg_script"],
)
"""
),
),
(
'mypkg_script',
DALS(
"""
#/usr/bin/python
print('mypkg_script')
"""
),
),
]
sdist_name = 'setuptools-test-script-1.0.zip'
sdist = str(tmpdir / sdist_name)
make_sdist(sdist, files)
return sdist
@pytest.mark.skipif(
not sys.platform.startswith('linux'), reason="Test can only be run on Linux"
)
def test_script_install(self, sdist_script, tmpdir, monkeypatch):
"""
Check scripts are installed.
"""
dist = Distribution({'script_args': ['easy_install']})
target = (tmpdir / 'target').ensure_dir()
cmd = ei.easy_install(
dist,
install_dir=str(target),
args=['x'],
)
monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target))
cmd.ensure_finalized()
cmd.easy_install(sdist_script)
assert (target / 'mypkg_script').exists()
@pytest.mark.filterwarnings('ignore:Unbuilt egg')
class TestPTHFileWriter:
def test_add_from_cwd_site_sets_dirty(self):
"""a pth file manager should set dirty
if a distribution is in site but also the cwd
"""
pth = PthDistributions('does-not_exist', [os.getcwd()])
assert not pth.dirty
pth.add(PRDistribution(os.getcwd()))
assert pth.dirty
def test_add_from_site_is_ignored(self):
location = '/test/location/does-not-have-to-exist'
# PthDistributions expects all locations to be normalized
location = pkg_resources.normalize_path(location)
pth = PthDistributions(
'does-not_exist',
[
location,
],
)
assert not pth.dirty
pth.add(PRDistribution(location))
assert not pth.dirty
def test_many_pth_distributions_merge_together(self, tmpdir):
"""
If the pth file is modified under the hood, then PthDistribution
will refresh its content before saving, merging contents when
necessary.
"""
# putting the pth file in a dedicated sub-folder,
pth_subdir = tmpdir.join("pth_subdir")
pth_subdir.mkdir()
pth_path = str(pth_subdir.join("file1.pth"))
pth1 = PthDistributions(pth_path)
pth2 = PthDistributions(pth_path)
assert pth1.paths == pth2.paths == [], (
"unless there would be some default added at some point"
)
# and so putting the src_subdir in folder distinct than the pth one,
# so to keep it absolute by PthDistributions
new_src_path = tmpdir.join("src_subdir")
new_src_path.mkdir() # must exist to be accounted
new_src_path_str = str(new_src_path)
pth1.paths.append(new_src_path_str)
pth1.save()
assert pth1.paths, (
"the new_src_path added must still be present/valid in pth1 after save"
)
# now,
assert new_src_path_str not in pth2.paths, (
"right before we save the entry should still not be present"
)
pth2.save()
assert new_src_path_str in pth2.paths, (
"the new_src_path entry should have been added by pth2 with its save() call"
)
assert pth2.paths[-1] == new_src_path, (
"and it should match exactly on the last entry actually "
"given we append to it in save()"
)
# finally,
assert PthDistributions(pth_path).paths == pth2.paths, (
"and we should have the exact same list at the end "
"with a fresh PthDistributions instance"
)
@pytest.fixture
def setup_context(tmpdir):
with (tmpdir / 'setup.py').open('w', encoding="utf-8") as f:
f.write(SETUP_PY)
with tmpdir.as_cwd():
yield tmpdir
@pytest.mark.usefixtures("user_override")
@pytest.mark.usefixtures("setup_context")
class TestUserInstallTest:
# prevent check that site-packages is writable. easy_install
# shouldn't be writing to system site-packages during finalize
# options, but while it does, bypass the behavior.
prev_sp_write = mock.patch(
'setuptools.command.easy_install.easy_install.check_site_dir',
mock.Mock(),
)
# simulate setuptools installed in user site packages
@mock.patch('setuptools.command.easy_install.__file__', site.USER_SITE)
@mock.patch('site.ENABLE_USER_SITE', True)
@prev_sp_write
def test_user_install_not_implied_user_site_enabled(self):
self.assert_not_user_site()
@mock.patch('site.ENABLE_USER_SITE', False)
@prev_sp_write
def test_user_install_not_implied_user_site_disabled(self):
self.assert_not_user_site()
@staticmethod
def assert_not_user_site():
# create a finalized easy_install command
dist = Distribution()
dist.script_name = 'setup.py'
cmd = ei.easy_install(dist)
cmd.args = ['py']
cmd.ensure_finalized()
assert not cmd.user, 'user should not be implied'
def test_multiproc_atexit(self):
pytest.importorskip('multiprocessing')
log = logging.getLogger('test_easy_install')
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
log.info('this should not break')
@pytest.fixture()
def foo_package(self, tmpdir):
egg_file = tmpdir / 'foo-1.0.egg-info'
with egg_file.open('w') as f:
f.write('Name: foo\n')
return str(tmpdir)
@pytest.fixture()
def install_target(self, tmpdir):
target = str(tmpdir)
with mock.patch('sys.path', sys.path + [target]):
python_path = os.path.pathsep.join(sys.path)
with mock.patch.dict(os.environ, PYTHONPATH=python_path):
yield target
def test_local_index(self, foo_package, install_target):
"""
The local index must be used when easy_install locates installed
packages.
"""
dist = Distribution()
dist.script_name = 'setup.py'
cmd = ei.easy_install(dist)
cmd.install_dir = install_target
cmd.args = ['foo']
cmd.ensure_finalized()
cmd.local_index.scan([foo_package])
res = cmd.easy_install('foo')
actual = os.path.normcase(os.path.realpath(res.location))
expected = os.path.normcase(os.path.realpath(foo_package))
assert actual == expected
@contextlib.contextmanager
def user_install_setup_context(self, *args, **kwargs):
"""
Wrap sandbox.setup_context to patch easy_install in that context to
appear as user-installed.
"""
with self.orig_context(*args, **kwargs):
import setuptools.command.easy_install as ei
ei.__file__ = site.USER_SITE
yield
def patched_setup_context(self):
self.orig_context = sandbox.setup_context
return mock.patch(
'setuptools.sandbox.setup_context',
self.user_install_setup_context,
)
@pytest.fixture
def distutils_package():
distutils_setup_py = SETUP_PY.replace(
'from setuptools import setup',
'from distutils.core import setup',
)
with contexts.tempdir(cd=os.chdir):
with open('setup.py', 'w', encoding="utf-8") as f:
f.write(distutils_setup_py)
yield
@pytest.fixture
def mock_index():
# set up a server which will simulate an alternate package index.
p_index = MockServer()
if p_index.server_port == 0:
# Some platforms (Jython) don't find a port to which to bind,
# so skip test for them.
pytest.skip("could not find a valid port")
p_index.start()
return p_index
class TestDistutilsPackage:
def test_bdist_egg_available_on_distutils_pkg(self, distutils_package):
run_setup('setup.py', ['bdist_egg'])
class TestInstallRequires:
def test_setup_install_includes_dependencies(self, tmp_path, mock_index):
"""
When ``python setup.py install`` is called directly, it will use easy_install
to fetch dependencies.
"""
# TODO: Remove these tests once `setup.py install` is completely removed
project_root = tmp_path / "project"
project_root.mkdir(exist_ok=True)
install_root = tmp_path / "install"
install_root.mkdir(exist_ok=True)
self.create_project(project_root)
cmd = [
sys.executable,
'-c',
'__import__("setuptools").setup()',
'install',
'--install-base',
str(install_root),
'--install-lib',
str(install_root),
'--install-headers',
str(install_root),
'--install-scripts',
str(install_root),
'--install-data',
str(install_root),
'--install-purelib',
str(install_root),
'--install-platlib',
str(install_root),
]
env = {**os.environ, "__EASYINSTALL_INDEX": mock_index.url}
cp = subprocess.run(
cmd,
cwd=str(project_root),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
)
assert cp.returncode != 0
try:
assert '/does-not-exist/' in {r.path for r in mock_index.requests}
assert next(
line
for line in cp.stdout.splitlines()
if "not find suitable distribution for" in line
and "does-not-exist" in line
)
except Exception:
if "failed to get random numbers" in cp.stdout:
pytest.xfail(f"{sys.platform} failure - {cp.stdout}")
raise
def create_project(self, root):
config = """
[metadata]
name = project
version = 42
[options]
install_requires = does-not-exist
py_modules = mod
"""
(root / 'setup.cfg').write_text(DALS(config), encoding="utf-8")
(root / 'mod.py').touch()
class TestSetupRequires:
def test_setup_requires_honors_fetch_params(self, mock_index, monkeypatch):
"""
When easy_install installs a source distribution which specifies
setup_requires, it should honor the fetch parameters (such as
index-url, and find-links).
"""
monkeypatch.setenv('PIP_RETRIES', '0')
monkeypatch.setenv('PIP_TIMEOUT', '0')
monkeypatch.setenv('PIP_NO_INDEX', 'false')
with contexts.quiet():
# create an sdist that has a build-time dependency.
with TestSetupRequires.create_sdist() as dist_file:
with contexts.tempdir() as temp_install_dir:
with contexts.environment(PYTHONPATH=temp_install_dir):
cmd = [
sys.executable,
'-c',
'__import__("setuptools").setup()',
'easy_install',
'--index-url',
mock_index.url,
'--exclude-scripts',
'--install-dir',
temp_install_dir,
dist_file,
]
subprocess.Popen(cmd).wait()
# there should have been one requests to the server
assert [r.path for r in mock_index.requests] == ['/does-not-exist/']
@staticmethod
@contextlib.contextmanager
def create_sdist():
"""
Return an sdist with a setup_requires dependency (of something that
doesn't exist)
"""
with contexts.tempdir() as dir:
dist_path = os.path.join(dir, 'setuptools-test-fetcher-1.0.tar.gz')
make_sdist(
dist_path,
[
(
'setup.py',
DALS(
"""
import setuptools
setuptools.setup(
name="setuptools-test-fetcher",
version="1.0",
setup_requires = ['does-not-exist'],
)
"""
),
),
('setup.cfg', ''),
],
)
yield dist_path
use_setup_cfg = (
(),
('dependency_links',),
('setup_requires',),
('dependency_links', 'setup_requires'),
)
@pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
def test_setup_requires_overrides_version_conflict(self, use_setup_cfg):
"""
Regression test for distribution issue 323:
https://bitbucket.org/tarek/distribute/issues/323
Ensures that a distribution's setup_requires requirements can still be
installed and used locally even if a conflicting version of that
requirement is already on the path.
"""
fake_dist = PRDistribution(
'does-not-matter', project_name='foobar', version='0.0'
)
working_set.add(fake_dist)
with contexts.save_pkg_resources_state():
with contexts.tempdir() as temp_dir:
test_pkg = create_setup_requires_package(
temp_dir, use_setup_cfg=use_setup_cfg
)
test_setup_py = os.path.join(test_pkg, 'setup.py')
with contexts.quiet() as (stdout, stderr):
# Don't even need to install the package, just
# running the setup.py at all is sufficient
run_setup(test_setup_py, ['--name'])
lines = stdout.readlines()
assert len(lines) > 0
assert lines[-1].strip() == 'test_pkg'
@pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
def test_setup_requires_override_nspkg(self, use_setup_cfg):
"""
Like ``test_setup_requires_overrides_version_conflict`` but where the
``setup_requires`` package is part of a namespace package that has
*already* been imported.
"""
with contexts.save_pkg_resources_state():
with contexts.tempdir() as temp_dir:
foobar_1_archive = os.path.join(temp_dir, 'foo_bar-0.1.tar.gz')
make_nspkg_sdist(foobar_1_archive, 'foo.bar', '0.1')
# Now actually go ahead an extract to the temp dir and add the
# extracted path to sys.path so foo.bar v0.1 is importable
foobar_1_dir = os.path.join(temp_dir, 'foo_bar-0.1')
os.mkdir(foobar_1_dir)
with tarfile.open(foobar_1_archive) as tf:
tf.extraction_filter = lambda member, path: member
tf.extractall(foobar_1_dir)
sys.path.insert(1, foobar_1_dir)
dist = PRDistribution(
foobar_1_dir, project_name='foo.bar', version='0.1'
)
working_set.add(dist)
template = DALS(
"""\
import foo # Even with foo imported first the
# setup_requires package should override
import setuptools
setuptools.setup(**%r)
if not (hasattr(foo, '__path__') and
len(foo.__path__) == 2):
print('FAIL')
if 'foo_bar-0.2' not in foo.__path__[0]:
print('FAIL')
"""
)
test_pkg = create_setup_requires_package(
temp_dir,
'foo.bar',
'0.2',
make_nspkg_sdist,
template,
use_setup_cfg=use_setup_cfg,
)
test_setup_py = os.path.join(test_pkg, 'setup.py')
with contexts.quiet() as (stdout, stderr):
try:
# Don't even need to install the package, just
# running the setup.py at all is sufficient
run_setup(test_setup_py, ['--name'])
except pkg_resources.VersionConflict: # pragma: nocover
pytest.fail(
'Installing setup.py requirements caused a VersionConflict'
)
assert 'FAIL' not in stdout.getvalue()
lines = stdout.readlines()
assert len(lines) > 0
assert lines[-1].strip() == 'test_pkg'
@pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
def test_setup_requires_with_attr_version(self, use_setup_cfg):
def make_dependency_sdist(dist_path, distname, version):
files = [
(
'setup.py',
DALS(
"""
import setuptools
setuptools.setup(
name={name!r},
version={version!r},
py_modules=[{name!r}],
)
""".format(name=distname, version=version)
),
),
(
distname + '.py',
DALS(
"""
version = 42
"""
),
),
]
make_sdist(dist_path, files)
with contexts.save_pkg_resources_state():
with contexts.tempdir() as temp_dir:
test_pkg = create_setup_requires_package(
temp_dir,
setup_attrs=dict(version='attr: foobar.version'),
make_package=make_dependency_sdist,
use_setup_cfg=use_setup_cfg + ('version',),
)
test_setup_py = os.path.join(test_pkg, 'setup.py')
with contexts.quiet() as (stdout, stderr):
run_setup(test_setup_py, ['--version'])
lines = stdout.readlines()
assert len(lines) > 0
assert lines[-1].strip() == '42'
def test_setup_requires_honors_pip_env(self, mock_index, monkeypatch):
monkeypatch.setenv('PIP_RETRIES', '0')
monkeypatch.setenv('PIP_TIMEOUT', '0')
monkeypatch.setenv('PIP_NO_INDEX', 'false')
monkeypatch.setenv('PIP_INDEX_URL', mock_index.url)
with contexts.save_pkg_resources_state():
with contexts.tempdir() as temp_dir:
test_pkg = create_setup_requires_package(
temp_dir,
'python-xlib',
'0.19',
setup_attrs=dict(dependency_links=[]),
)
test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
with open(test_setup_cfg, 'w', encoding="utf-8") as fp:
fp.write(
DALS(
"""
[easy_install]
index_url = https://pypi.org/legacy/
"""
)
)
test_setup_py = os.path.join(test_pkg, 'setup.py')
with pytest.raises(distutils.errors.DistutilsError):
run_setup(test_setup_py, ['--version'])
assert len(mock_index.requests) == 1
assert mock_index.requests[0].path == '/python-xlib/'
def test_setup_requires_with_pep508_url(self, mock_index, monkeypatch):
monkeypatch.setenv('PIP_RETRIES', '0')
monkeypatch.setenv('PIP_TIMEOUT', '0')
monkeypatch.setenv('PIP_INDEX_URL', mock_index.url)
with contexts.save_pkg_resources_state():
with contexts.tempdir() as temp_dir:
dep_sdist = os.path.join(temp_dir, 'dep.tar.gz')
make_trivial_sdist(dep_sdist, 'dependency', '42')
dep_url = path_to_url(dep_sdist, authority='localhost')
test_pkg = create_setup_requires_package(
temp_dir,
# Ignored (overridden by setup_attrs)
'python-xlib',
'0.19',
setup_attrs=dict(setup_requires='dependency @ %s' % dep_url),
)
test_setup_py = os.path.join(test_pkg, 'setup.py')
run_setup(test_setup_py, ['--version'])
assert len(mock_index.requests) == 0
def test_setup_requires_with_allow_hosts(self, mock_index):
"""The `allow-hosts` option in not supported anymore."""
files = {
'test_pkg': {
'setup.py': DALS(
"""
from setuptools import setup
setup(setup_requires='python-xlib')
"""
),
'setup.cfg': DALS(
"""
[easy_install]
allow_hosts = *
"""
),
}
}
with contexts.save_pkg_resources_state():
with contexts.tempdir() as temp_dir:
path.build(files, prefix=temp_dir)
setup_py = str(pathlib.Path(temp_dir, 'test_pkg', 'setup.py'))
with pytest.raises(distutils.errors.DistutilsError):
run_setup(setup_py, ['--version'])
assert len(mock_index.requests) == 0
def test_setup_requires_with_python_requires(self, monkeypatch, tmpdir):
"""Check `python_requires` is honored."""
monkeypatch.setenv('PIP_RETRIES', '0')
monkeypatch.setenv('PIP_TIMEOUT', '0')
monkeypatch.setenv('PIP_NO_INDEX', '1')
monkeypatch.setenv('PIP_VERBOSE', '1')
dep_1_0_sdist = 'dep-1.0.tar.gz'
dep_1_0_url = path_to_url(str(tmpdir / dep_1_0_sdist))
dep_1_0_python_requires = '>=2.7'
make_python_requires_sdist(
str(tmpdir / dep_1_0_sdist), 'dep', '1.0', dep_1_0_python_requires
)
dep_2_0_sdist = 'dep-2.0.tar.gz'
dep_2_0_url = path_to_url(str(tmpdir / dep_2_0_sdist))
dep_2_0_python_requires = '!=' + '.'.join(map(str, sys.version_info[:2])) + '.*'
make_python_requires_sdist(
str(tmpdir / dep_2_0_sdist), 'dep', '2.0', dep_2_0_python_requires
)
index = tmpdir / 'index.html'
index.write_text(
DALS(
"""
<!DOCTYPE html>
<html><head><title>Links for dep</title></head>
<body>
<h1>Links for dep</h1>
<a href="{dep_1_0_url}"\
data-requires-python="{dep_1_0_python_requires}">{dep_1_0_sdist}</a><br/>
<a href="{dep_2_0_url}"\
data-requires-python="{dep_2_0_python_requires}">{dep_2_0_sdist}</a><br/>
</body>
</html>
"""
).format(
dep_1_0_url=dep_1_0_url,
dep_1_0_sdist=dep_1_0_sdist,
dep_1_0_python_requires=dep_1_0_python_requires,
dep_2_0_url=dep_2_0_url,
dep_2_0_sdist=dep_2_0_sdist,
dep_2_0_python_requires=dep_2_0_python_requires,
),
'utf-8',
)
index_url = path_to_url(str(index))
with contexts.save_pkg_resources_state():
test_pkg = create_setup_requires_package(
str(tmpdir),
'python-xlib',
'0.19', # Ignored (overridden by setup_attrs).
setup_attrs=dict(setup_requires='dep', dependency_links=[index_url]),
)
test_setup_py = os.path.join(test_pkg, 'setup.py')
run_setup(test_setup_py, ['--version'])
eggs = list(
map(str, pkg_resources.find_distributions(os.path.join(test_pkg, '.eggs')))
)
assert eggs == ['dep 1.0']
@pytest.mark.parametrize('with_dependency_links_in_setup_py', (False, True))
def test_setup_requires_with_find_links_in_setup_cfg(
self, monkeypatch, with_dependency_links_in_setup_py
):
monkeypatch.setenv('PIP_RETRIES', '0')
monkeypatch.setenv('PIP_TIMEOUT', '0')
with contexts.save_pkg_resources_state():
with contexts.tempdir() as temp_dir:
make_trivial_sdist(
os.path.join(temp_dir, 'python-xlib-42.tar.gz'), 'python-xlib', '42'
)
test_pkg = os.path.join(temp_dir, 'test_pkg')
test_setup_py = os.path.join(test_pkg, 'setup.py')
test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
os.mkdir(test_pkg)
with open(test_setup_py, 'w', encoding="utf-8") as fp:
if with_dependency_links_in_setup_py:
dependency_links = [os.path.join(temp_dir, 'links')]
else:
dependency_links = []
fp.write(
DALS(
"""
from setuptools import installer, setup
setup(setup_requires='python-xlib==42',
dependency_links={dependency_links!r})
"""
).format(dependency_links=dependency_links)
)
with open(test_setup_cfg, 'w', encoding="utf-8") as fp:
fp.write(
DALS(
"""
[easy_install]
index_url = {index_url}
find_links = {find_links}
"""
).format(
index_url=os.path.join(temp_dir, 'index'),
find_links=temp_dir,
)
)
run_setup(test_setup_py, ['--version'])
def test_setup_requires_with_transitive_extra_dependency(self, monkeypatch):
"""
Use case: installing a package with a build dependency on
an already installed `dep[extra]`, which in turn depends
on `extra_dep` (whose is not already installed).
"""
with contexts.save_pkg_resources_state():
with contexts.tempdir() as temp_dir:
# Create source distribution for `extra_dep`.
make_trivial_sdist(
os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'), 'extra_dep', '1.0'
)
# Create source tree for `dep`.
dep_pkg = os.path.join(temp_dir, 'dep')
os.mkdir(dep_pkg)
path.build(
{
'setup.py': DALS(
"""
import setuptools
setuptools.setup(
name='dep', version='2.0',
extras_require={'extra': ['extra_dep']},
)
"""
),
'setup.cfg': '',
},
prefix=dep_pkg,
)
# "Install" dep.
run_setup(os.path.join(dep_pkg, 'setup.py'), ['dist_info'])
working_set.add_entry(dep_pkg)
# Create source tree for test package.
test_pkg = os.path.join(temp_dir, 'test_pkg')
test_setup_py = os.path.join(test_pkg, 'setup.py')
os.mkdir(test_pkg)
with open(test_setup_py, 'w', encoding="utf-8") as fp:
fp.write(
DALS(
"""
from setuptools import installer, setup
setup(setup_requires='dep[extra]')
"""
)
)
# Check...
monkeypatch.setenv('PIP_FIND_LINKS', str(temp_dir))
monkeypatch.setenv('PIP_NO_INDEX', '1')
monkeypatch.setenv('PIP_RETRIES', '0')
monkeypatch.setenv('PIP_TIMEOUT', '0')
run_setup(test_setup_py, ['--version'])
def test_setup_requires_with_distutils_command_dep(self, monkeypatch):
"""
Use case: ensure build requirements' extras
are properly installed and activated.
"""
with contexts.save_pkg_resources_state():
with contexts.tempdir() as temp_dir:
# Create source distribution for `extra_dep`.
make_sdist(
os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'),
[
(
'setup.py',
DALS(
"""
import setuptools
setuptools.setup(
name='extra_dep',
version='1.0',
py_modules=['extra_dep'],
)
"""
),
),
('setup.cfg', ''),
('extra_dep.py', ''),
],
)
# Create source tree for `epdep`.
dep_pkg = os.path.join(temp_dir, 'epdep')
os.mkdir(dep_pkg)
path.build(
{
'setup.py': DALS(
"""
import setuptools
setuptools.setup(
name='dep', version='2.0',
py_modules=['epcmd'],
extras_require={'extra': ['extra_dep']},
entry_points='''
[distutils.commands]
epcmd = epcmd:epcmd [extra]
''',
)
"""
),
'setup.cfg': '',
'epcmd.py': DALS(
"""
from distutils.command.build_py import build_py
import extra_dep
class epcmd(build_py):
pass
"""
),
},
prefix=dep_pkg,
)
# "Install" dep.
run_setup(os.path.join(dep_pkg, 'setup.py'), ['dist_info'])
working_set.add_entry(dep_pkg)
# Create source tree for test package.
test_pkg = os.path.join(temp_dir, 'test_pkg')
test_setup_py = os.path.join(test_pkg, 'setup.py')
os.mkdir(test_pkg)
with open(test_setup_py, 'w', encoding="utf-8") as fp:
fp.write(
DALS(
"""
from setuptools import installer, setup
setup(setup_requires='dep[extra]')
"""
)
)
# Check...
monkeypatch.setenv('PIP_FIND_LINKS', str(temp_dir))
monkeypatch.setenv('PIP_NO_INDEX', '1')
monkeypatch.setenv('PIP_RETRIES', '0')
monkeypatch.setenv('PIP_TIMEOUT', '0')
run_setup(test_setup_py, ['epcmd'])
def make_trivial_sdist(dist_path, distname, version):
"""
Create a simple sdist tarball at dist_path, containing just a simple
setup.py.
"""
make_sdist(
dist_path,
[
(
'setup.py',
DALS(
"""\
import setuptools
setuptools.setup(
name=%r,
version=%r
)
"""
% (distname, version)
),
),
('setup.cfg', ''),
],
)
def make_nspkg_sdist(dist_path, distname, version):
"""
Make an sdist tarball with distname and version which also contains one
package with the same name as distname. The top-level package is
designated a namespace package).
"""
# Assert that the distname contains at least one period
assert '.' in distname
parts = distname.split('.')
nspackage = parts[0]
packages = ['.'.join(parts[:idx]) for idx in range(1, len(parts) + 1)]
setup_py = DALS(
"""\
import setuptools
setuptools.setup(
name=%r,
version=%r,
packages=%r,
namespace_packages=[%r]
)
"""
% (distname, version, packages, nspackage)
)
init = "__import__('pkg_resources').declare_namespace(__name__)"
files = [('setup.py', setup_py), (os.path.join(nspackage, '__init__.py'), init)]
for package in packages[1:]:
filename = os.path.join(*(package.split('.') + ['__init__.py']))
files.append((filename, ''))
make_sdist(dist_path, files)
def make_python_requires_sdist(dist_path, distname, version, python_requires):
make_sdist(
dist_path,
[
(
'setup.py',
DALS(
"""\
import setuptools
setuptools.setup(
name={name!r},
version={version!r},
python_requires={python_requires!r},
)
"""
).format(
name=distname, version=version, python_requires=python_requires
),
),
('setup.cfg', ''),
],
)
def make_sdist(dist_path, files):
"""
Create a simple sdist tarball at dist_path, containing the files
listed in ``files`` as ``(filename, content)`` tuples.
"""
# Distributions with only one file don't play well with pip.
assert len(files) > 1
with tarfile.open(dist_path, 'w:gz') as dist:
for filename, content in files:
file_bytes = io.BytesIO(content.encode('utf-8'))
file_info = tarfile.TarInfo(name=filename)
file_info.size = len(file_bytes.getvalue())
file_info.mtime = int(time.time())
dist.addfile(file_info, fileobj=file_bytes)
def create_setup_requires_package(
path,
distname='foobar',
version='0.1',
make_package=make_trivial_sdist,
setup_py_template=None,
setup_attrs=None,
use_setup_cfg=(),
):
"""Creates a source tree under path for a trivial test package that has a
single requirement in setup_requires--a tarball for that requirement is
also created and added to the dependency_links argument.
``distname`` and ``version`` refer to the name/version of the package that
the test package requires via ``setup_requires``. The name of the test
package itself is just 'test_pkg'.
"""
normalized_distname = safer_name(distname)
test_setup_attrs = {
'name': 'test_pkg',
'version': '0.0',
'setup_requires': [f'{normalized_distname}=={version}'],
'dependency_links': [os.path.abspath(path)],
}
if setup_attrs:
test_setup_attrs.update(setup_attrs)
test_pkg = os.path.join(path, 'test_pkg')
os.mkdir(test_pkg)
# setup.cfg
if use_setup_cfg:
options = []
metadata = []
for name in use_setup_cfg:
value = test_setup_attrs.pop(name)
if name in 'name version'.split():
section = metadata
else:
section = options
if isinstance(value, (tuple, list)):
value = ';'.join(value)
section.append('%s: %s' % (name, value))
test_setup_cfg_contents = DALS(
"""
[metadata]
{metadata}
[options]
{options}
"""
).format(
options='\n'.join(options),
metadata='\n'.join(metadata),
)
else:
test_setup_cfg_contents = ''
with open(os.path.join(test_pkg, 'setup.cfg'), 'w', encoding="utf-8") as f:
f.write(test_setup_cfg_contents)
# setup.py
if setup_py_template is None:
setup_py_template = DALS(
"""\
import setuptools
setuptools.setup(**%r)
"""
)
with open(os.path.join(test_pkg, 'setup.py'), 'w', encoding="utf-8") as f:
f.write(setup_py_template % test_setup_attrs)
foobar_path = os.path.join(path, f'{normalized_distname}-{version}.tar.gz')
make_package(foobar_path, distname, version)
return test_pkg
@pytest.mark.skipif(
sys.platform.startswith('java') and ei.is_sh(sys.executable),
reason="Test cannot run under java when executable is sh",
)
class TestScriptHeader:
non_ascii_exe = '/Users/José/bin/python'
exe_with_spaces = r'C:\Program Files\Python36\python.exe'
def test_get_script_header(self):
expected = '#!%s\n' % ei.nt_quote_arg(os.path.normpath(sys.executable))
actual = ei.ScriptWriter.get_header('#!/usr/local/bin/python')
assert actual == expected
def test_get_script_header_args(self):
expected = '#!%s -x\n' % ei.nt_quote_arg(os.path.normpath(sys.executable))
actual = ei.ScriptWriter.get_header('#!/usr/bin/python -x')
assert actual == expected
def test_get_script_header_non_ascii_exe(self):
actual = ei.ScriptWriter.get_header(
'#!/usr/bin/python', executable=self.non_ascii_exe
)
expected = '#!%s -x\n' % self.non_ascii_exe
assert actual == expected
def test_get_script_header_exe_with_spaces(self):
actual = ei.ScriptWriter.get_header(
'#!/usr/bin/python', executable='"' + self.exe_with_spaces + '"'
)
expected = '#!"%s"\n' % self.exe_with_spaces
assert actual == expected
class TestCommandSpec:
def test_custom_launch_command(self):
"""
Show how a custom CommandSpec could be used to specify a #! executable
which takes parameters.
"""
cmd = ei.CommandSpec(['/usr/bin/env', 'python3'])
assert cmd.as_header() == '#!/usr/bin/env python3\n'
def test_from_param_for_CommandSpec_is_passthrough(self):
"""
from_param should return an instance of a CommandSpec
"""
cmd = ei.CommandSpec(['python'])
cmd_new = ei.CommandSpec.from_param(cmd)
assert cmd is cmd_new
@mock.patch('sys.executable', TestScriptHeader.exe_with_spaces)
@mock.patch.dict(os.environ)
def test_from_environment_with_spaces_in_executable(self):
os.environ.pop('__PYVENV_LAUNCHER__', None)
cmd = ei.CommandSpec.from_environment()
assert len(cmd) == 1
assert cmd.as_header().startswith('#!"')
def test_from_simple_string_uses_shlex(self):
"""
In order to support `executable = /usr/bin/env my-python`, make sure
from_param invokes shlex on that input.
"""
cmd = ei.CommandSpec.from_param('/usr/bin/env my-python')
assert len(cmd) == 2
assert '"' not in cmd.as_header()
def test_from_param_raises_expected_error(self) -> None:
"""
from_param should raise its own TypeError when the argument's type is unsupported
"""
with pytest.raises(TypeError) as exc_info:
ei.CommandSpec.from_param(object()) # type: ignore[arg-type] # We want a type error here
assert (
str(exc_info.value) == "Argument has an unsupported type <class 'object'>"
), exc_info.value
class TestWindowsScriptWriter:
def test_header(self):
hdr = ei.WindowsScriptWriter.get_header('')
assert hdr.startswith('#!')
assert hdr.endswith('\n')
hdr = hdr.lstrip('#!')
hdr = hdr.rstrip('\n')
# header should not start with an escaped quote
assert not hdr.startswith('\\"')
class VersionStub(NamedTuple):
major: int
minor: int
micro: int
releaselevel: str
serial: int
def test_use_correct_python_version_string(tmpdir, tmpdir_cwd, monkeypatch):
# In issue #3001, easy_install wrongly uses the `python3.1` directory
# when the interpreter is `python3.10` and the `--user` option is given.
# See pypa/setuptools#3001.
dist = Distribution()
cmd = dist.get_command_obj('easy_install')
cmd.args = ['ok']
cmd.optimize = 0
cmd.user = True
cmd.install_userbase = str(tmpdir)
cmd.install_usersite = None
install_cmd = dist.get_command_obj('install')
install_cmd.install_userbase = str(tmpdir)
install_cmd.install_usersite = None
with monkeypatch.context() as patch, warnings.catch_warnings():
warnings.simplefilter("ignore")
version = '3.10.1 (main, Dec 21 2021, 09:17:12) [GCC 10.2.1 20210110]'
info = VersionStub(3, 10, 1, "final", 0)
patch.setattr('site.ENABLE_USER_SITE', True)
patch.setattr('sys.version', version)
patch.setattr('sys.version_info', info)
patch.setattr(cmd, 'create_home_path', mock.Mock())
cmd.finalize_options()
name = "pypy" if hasattr(sys, 'pypy_version_info') else "python"
install_dir = cmd.install_dir.lower()
# In some platforms (e.g. Windows), install_dir is mostly determined
# via `sysconfig`, which define constants eagerly at module creation.
# This means that monkeypatching `sys.version` to emulate 3.10 for testing
# may have no effect.
# The safest test here is to rely on the fact that 3.1 is no longer
# supported/tested, and make sure that if 'python3.1' ever appears in the string
# it is followed by another digit (e.g. 'python3.10').
if re.search(name + r'3\.?1', install_dir):
assert re.search(name + r'3\.?1\d', install_dir)
# The following "variables" are used for interpolation in distutils
# installation schemes, so it should be fair to treat them as "semi-public",
# or at least public enough so we can have a test to make sure they are correct
assert cmd.config_vars['py_version'] == '3.10.1'
assert cmd.config_vars['py_version_short'] == '3.10'
assert cmd.config_vars['py_version_nodot'] == '310'
def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmp_path):
"""`setup.py develop` should honor `--user` even under build isolation"""
# == Arrange ==
# Pretend that build isolation was enabled
# e.g pip sets the environment variable PYTHONNOUSERSITE=1
monkeypatch.setattr('site.ENABLE_USER_SITE', False)
# Patching $HOME for 2 reasons:
# 1. setuptools/command/easy_install.py:create_home_path
# tries creating directories in $HOME.
# Given::
# self.config_vars['DESTDIRS'] = (
# "/home/user/.pyenv/versions/3.9.10 "
# "/home/user/.pyenv/versions/3.9.10/lib "
# "/home/user/.pyenv/versions/3.9.10/lib/python3.9 "
# "/home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload")
# `create_home_path` will::
# makedirs(
# "/home/user/.pyenv/versions/3.9.10 "
# "/home/user/.pyenv/versions/3.9.10/lib "
# "/home/user/.pyenv/versions/3.9.10/lib/python3.9 "
# "/home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload")
#
# 2. We are going to force `site` to update site.USER_BASE and site.USER_SITE
# To point inside our new home
monkeypatch.setenv('HOME', str(tmp_path / '.home'))
monkeypatch.setenv('USERPROFILE', str(tmp_path / '.home'))
monkeypatch.setenv('APPDATA', str(tmp_path / '.home'))
monkeypatch.setattr('site.USER_BASE', None)
monkeypatch.setattr('site.USER_SITE', None)
user_site = Path(site.getusersitepackages())
user_site.mkdir(parents=True, exist_ok=True)
sys_prefix = tmp_path / '.sys_prefix'
sys_prefix.mkdir(parents=True, exist_ok=True)
monkeypatch.setattr('sys.prefix', str(sys_prefix))
setup_script = (
"__import__('setuptools').setup(name='aproj', version=42, packages=[])\n"
)
(tmp_path / "setup.py").write_text(setup_script, encoding="utf-8")
# == Sanity check ==
assert list(sys_prefix.glob("*")) == []
assert list(user_site.glob("*")) == []
# == Act ==
run_setup('setup.py', ['develop', '--user'])
# == Assert ==
# Should not install to sys.prefix
assert list(sys_prefix.glob("*")) == []
# Should install to user site
installed = {f.name for f in user_site.glob("*")}
# sometimes easy-install.pth is created and sometimes not
installed = installed - {"easy-install.pth"}
assert installed == {'aproj.egg-link'}
import http.client
import urllib.error
import urllib.request
from inspect import cleandoc
import pytest
import setuptools.package_index
import distutils.errors
class TestPackageIndex:
def test_regex(self):
hash_url = 'http://other_url?:action=show_md5&amp;'
hash_url += 'digest=0123456789abcdef0123456789abcdef'
doc = """
<a href="http://some_url">Name</a>
(<a title="MD5 hash"
href="{hash_url}">md5</a>)
""".lstrip().format(**locals())
assert setuptools.package_index.PYPI_MD5.match(doc)
def test_bad_url_bad_port(self):
index = setuptools.package_index.PackageIndex()
url = 'http://127.0.0.1:0/nonesuch/test_package_index'
try:
v = index.open_url(url)
except Exception as exc:
assert url in str(exc)
else:
assert isinstance(v, urllib.error.HTTPError)
def test_bad_url_typo(self):
# issue 16
# easy_install inquant.contentmirror.plone breaks because of a typo
# in its home URL
index = setuptools.package_index.PackageIndex(hosts=('www.example.com',))
url = (
'url:%20https://svn.plone.org/svn'
'/collective/inquant.contentmirror.plone/trunk'
)
try:
v = index.open_url(url)
except Exception as exc:
assert url in str(exc)
else:
assert isinstance(v, urllib.error.HTTPError)
def test_bad_url_bad_status_line(self):
index = setuptools.package_index.PackageIndex(hosts=('www.example.com',))
def _urlopen(*args):
raise http.client.BadStatusLine('line')
index.opener = _urlopen
url = 'http://example.com'
try:
index.open_url(url)
except Exception as exc:
assert 'line' in str(exc)
else:
raise AssertionError('Should have raise here!')
def test_bad_url_double_scheme(self):
"""
A bad URL with a double scheme should raise a DistutilsError.
"""
index = setuptools.package_index.PackageIndex(hosts=('www.example.com',))
# issue 20
url = 'http://http://svn.pythonpaste.org/Paste/wphp/trunk'
try:
index.open_url(url)
except distutils.errors.DistutilsError as error:
msg = str(error)
assert (
'nonnumeric port' in msg
or 'getaddrinfo failed' in msg
or 'Name or service not known' in msg
)
return
raise RuntimeError("Did not raise")
def test_url_ok(self):
index = setuptools.package_index.PackageIndex(hosts=('www.example.com',))
url = 'file:///tmp/test_package_index'
assert index.url_ok(url, True)
def test_parse_bdist_wininst(self):
parse = setuptools.package_index.parse_bdist_wininst
actual = parse('reportlab-2.5.win32-py2.4.exe')
expected = 'reportlab-2.5', '2.4', 'win32'
assert actual == expected
actual = parse('reportlab-2.5.win32.exe')
expected = 'reportlab-2.5', None, 'win32'
assert actual == expected
actual = parse('reportlab-2.5.win-amd64-py2.7.exe')
expected = 'reportlab-2.5', '2.7', 'win-amd64'
assert actual == expected
actual = parse('reportlab-2.5.win-amd64.exe')
expected = 'reportlab-2.5', None, 'win-amd64'
assert actual == expected
def test__vcs_split_rev_from_url(self):
"""
Test the basic usage of _vcs_split_rev_from_url
"""
vsrfu = setuptools.package_index.PackageIndex._vcs_split_rev_from_url
url, rev = vsrfu('https://example.com/bar@2995')
assert url == 'https://example.com/bar'
assert rev == '2995'
def test_local_index(self, tmpdir):
"""
local_open should be able to read an index from the file system.
"""
index_file = tmpdir / 'index.html'
with index_file.open('w') as f:
f.write('<div>content</div>')
url = 'file:' + urllib.request.pathname2url(str(tmpdir)) + '/'
res = setuptools.package_index.local_open(url)
assert 'content' in res.read()
def test_egg_fragment(self):
"""
EGG fragments must comply to PEP 440
"""
epoch = [
'',
'1!',
]
releases = [
'0',
'0.0',
'0.0.0',
]
pre = [
'a0',
'b0',
'rc0',
]
post = ['.post0']
dev = [
'.dev0',
]
local = [
('', ''),
('+ubuntu.0', '+ubuntu.0'),
('+ubuntu-0', '+ubuntu.0'),
('+ubuntu_0', '+ubuntu.0'),
]
versions = [
[''.join([e, r, p, loc]) for loc in locs]
for e in epoch
for r in releases
for p in sum([pre, post, dev], [''])
for locs in local
]
for v, vc in versions:
dists = list(
setuptools.package_index.distros_for_url(
'http://example.com/example-foo.zip#egg=example-foo-' + v
)
)
assert dists[0].version == ''
assert dists[1].version == vc
def test_download_git_with_rev(self, tmp_path, fp):
url = 'git+https://github.example/group/project@master#egg=foo'
index = setuptools.package_index.PackageIndex()
expected_dir = tmp_path / 'project@master'
fp.register([
'git',
'clone',
'--quiet',
'https://github.example/group/project',
expected_dir,
])
fp.register(['git', '-C', expected_dir, 'checkout', '--quiet', 'master'])
result = index.download(url, tmp_path)
assert result == str(expected_dir)
assert len(fp.calls) == 2
def test_download_git_no_rev(self, tmp_path, fp):
url = 'git+https://github.example/group/project#egg=foo'
index = setuptools.package_index.PackageIndex()
expected_dir = tmp_path / 'project'
fp.register([
'git',
'clone',
'--quiet',
'https://github.example/group/project',
expected_dir,
])
index.download(url, tmp_path)
def test_download_svn(self, tmp_path):
url = 'svn+https://svn.example/project#egg=foo'
index = setuptools.package_index.PackageIndex()
msg = r".*SVN download is not supported.*"
with pytest.raises(distutils.errors.DistutilsError, match=msg):
index.download(url, tmp_path)
class TestContentCheckers:
def test_md5(self):
checker = setuptools.package_index.HashChecker.from_url(
'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478'
)
checker.feed('You should probably not be using MD5'.encode('ascii'))
assert checker.hash.hexdigest() == 'f12895fdffbd45007040d2e44df98478'
assert checker.is_valid()
def test_other_fragment(self):
"Content checks should succeed silently if no hash is present"
checker = setuptools.package_index.HashChecker.from_url(
'http://foo/bar#something%20completely%20different'
)
checker.feed('anything'.encode('ascii'))
assert checker.is_valid()
def test_blank_md5(self):
"Content checks should succeed if a hash is empty"
checker = setuptools.package_index.HashChecker.from_url('http://foo/bar#md5=')
checker.feed('anything'.encode('ascii'))
assert checker.is_valid()
def test_get_hash_name_md5(self):
checker = setuptools.package_index.HashChecker.from_url(
'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478'
)
assert checker.hash_name == 'md5'
def test_report(self):
checker = setuptools.package_index.HashChecker.from_url(
'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478'
)
rep = checker.report(lambda x: x, 'My message about %s')
assert rep == 'My message about md5'
class TestPyPIConfig:
def test_percent_in_password(self, tmp_home_dir):
pypirc = tmp_home_dir / '.pypirc'
pypirc.write_text(
cleandoc(
"""
[pypi]
repository=https://pypi.org
username=jaraco
password=pity%
"""
),
encoding="utf-8",
)
cfg = setuptools.package_index.PyPIConfig()
cred = cfg.creds_by_repository['https://pypi.org']
assert cred.username == 'jaraco'
assert cred.password == 'pity%'
@pytest.mark.timeout(1)
def test_REL_DoS():
"""
REL should not hang on a contrived attack string.
"""
setuptools.package_index.REL.search('< rel=' + ' ' * 2**12)
"""develop tests"""
import os
import types
import pytest
import pkg_resources
import setuptools.sandbox
class TestSandbox:
def test_devnull(self, tmpdir):
with setuptools.sandbox.DirectorySandbox(str(tmpdir)):
self._file_writer(os.devnull)
@staticmethod
def _file_writer(path):
def do_write():
with open(path, 'w', encoding="utf-8") as f:
f.write('xxx')
return do_write
def test_setup_py_with_BOM(self):
"""
It should be possible to execute a setup.py with a Byte Order Mark
"""
target = pkg_resources.resource_filename(__name__, 'script-with-bom.py')
namespace = types.ModuleType('namespace')
setuptools.sandbox._execfile(target, vars(namespace))
assert namespace.result == 'passed'
def test_setup_py_with_CRLF(self, tmpdir):
setup_py = tmpdir / 'setup.py'
with setup_py.open('wb') as stream:
stream.write(b'"degenerate script"\r\n')
setuptools.sandbox._execfile(str(setup_py), globals())
class TestExceptionSaver:
def test_exception_trapped(self):
with setuptools.sandbox.ExceptionSaver():
raise ValueError("details")
def test_exception_resumed(self):
with setuptools.sandbox.ExceptionSaver() as saved_exc:
raise ValueError("details")
with pytest.raises(ValueError) as caught:
saved_exc.resume()
assert isinstance(caught.value, ValueError)
assert str(caught.value) == 'details'
def test_exception_reconstructed(self):
orig_exc = ValueError("details")
with setuptools.sandbox.ExceptionSaver() as saved_exc:
raise orig_exc
with pytest.raises(ValueError) as caught:
saved_exc.resume()
assert isinstance(caught.value, ValueError)
assert caught.value is not orig_exc
def test_no_exception_passes_quietly(self):
with setuptools.sandbox.ExceptionSaver() as saved_exc:
pass
saved_exc.resume()
def test_unpickleable_exception(self):
class CantPickleThis(Exception):
"This Exception is unpickleable because it's not in globals"
def __repr__(self) -> str:
return 'CantPickleThis%r' % (self.args,)
with setuptools.sandbox.ExceptionSaver() as saved_exc:
raise CantPickleThis('detail')
with pytest.raises(setuptools.sandbox.UnpickleableException) as caught:
saved_exc.resume()
assert str(caught.value) == "CantPickleThis('detail',)"
def test_unpickleable_exception_when_hiding_setuptools(self):
"""
As revealed in #440, an infinite recursion can occur if an unpickleable
exception while setuptools is hidden. Ensure this doesn't happen.
"""
class ExceptionUnderTest(Exception):
"""
An unpickleable exception (not in globals).
"""
with pytest.raises(setuptools.sandbox.UnpickleableException) as caught:
with setuptools.sandbox.save_modules():
setuptools.sandbox.hide_setuptools()
raise ExceptionUnderTest
(msg,) = caught.value.args
assert msg == 'ExceptionUnderTest()'
def test_sandbox_violation_raised_hiding_setuptools(self, tmpdir):
"""
When in a sandbox with setuptools hidden, a SandboxViolation
should reflect a proper exception and not be wrapped in
an UnpickleableException.
"""
def write_file():
"Trigger a SandboxViolation by writing outside the sandbox"
with open('/etc/foo', 'w', encoding="utf-8"):
pass
with pytest.raises(setuptools.sandbox.SandboxViolation) as caught:
with setuptools.sandbox.save_modules():
setuptools.sandbox.hide_setuptools()
with setuptools.sandbox.DirectorySandbox(str(tmpdir)):
write_file()
cmd, args, kwargs = caught.value.args
assert cmd == 'open'
assert args == ('/etc/foo', 'w')
assert kwargs == {"encoding": "utf-8"}
msg = str(caught.value)
assert 'open' in msg
assert "('/etc/foo', 'w')" in msg
assert "{'encoding': 'utf-8'}" in msg
+3
-2

@@ -6,3 +6,4 @@ # don't import any costly modules

report_url = (
"https://github.com/pypa/setuptools/issues/new?template=distutils-deprecation.yml"
"https://github.com/pypa/setuptools/issues/new?"
"template=distutils-deprecation.yml"
)

@@ -94,3 +95,3 @@

class _TrivialRe:
def __init__(self, *patterns) -> None:
def __init__(self, *patterns):
self._patterns = patterns

@@ -97,0 +98,0 @@

+0
-4

@@ -139,5 +139,2 @@ from __future__ import annotations

},
"source_repository": "https://github.com/pypa/setuptools/",
"source_branch": "main",
"source_directory": "docs/",
}

@@ -245,3 +242,2 @@

'PyPUG': ('https://packaging.python.org/en/latest', None),
'pytest': ('https://docs.pytest.org/en/stable', None),
'packaging': ('https://packaging.pypa.io/en/latest', None),

@@ -248,0 +244,0 @@ 'twine': ('https://twine.readthedocs.io/en/stable', None),

@@ -200,3 +200,10 @@ Development Mode (a.k.a. "Editable Installs")

.. note::
Newer versions of ``pip`` no longer run the fallback command
``python setup.py develop`` when the ``pyproject.toml`` file is present.
This means that setting the environment variable
``SETUPTOOLS_ENABLE_FEATURES="legacy-editable"``
will have no effect when installing a package with ``pip``.
How editable installations work

@@ -233,21 +240,2 @@ -------------------------------

Debugging Tips
--------------
If encountering problems installing a project in editable mode,
follow these recommended steps to help debug:
- Try to install the project normally, without using the editable mode.
Does the error still persist?
(If it does, try fixing the problem before attempting the editable mode).
- When using binary extensions, make sure all OS-level
dependencies are installed (e.g. compilers, toolchains, binary libraries, ...).
- Try the latest version of setuptools (maybe the error was already fixed).
- When the project or its dependencies are using any setuptools extension
or customization, make sure they support the editable mode.
After following the steps above, if the problem still persists and
you think this is related to how setuptools handles editable installations,
please submit a `reproducible example <https://stackoverflow.com/help/minimal-reproducible-example>`_ at `the bug tracker <https://github.com/pypa/setuptools/issues>`_.
----

@@ -254,0 +242,0 @@

@@ -113,3 +113,4 @@ ==========================

* first, the options provided by the environment variables ``CFLAGS`` and ``CPPFLAGS``,
* first, the options provided by the ``sysconfig`` variable ``CFLAGS``,
* then, the options provided by the environment variables ``CFLAGS`` and ``CPPFLAGS``,
* then, the options provided by the ``sysconfig`` variable ``CCSHARED``,

@@ -116,0 +117,0 @@ * then, a ``-I`` option for each element of ``Extension.include_dirs``,

@@ -125,3 +125,3 @@ .. _Creating ``distutils`` Extensions:

raise SetupError(
f"{attr!r} must be a boolean value (got {value!r}"
"%r must be a boolean value (got %r)" % (attr,value)
)

@@ -128,0 +128,0 @@

@@ -28,3 +28,2 @@ ==================================================

quickstart
interfaces
package_discovery

@@ -31,0 +30,0 @@ dependency_management

@@ -27,3 +27,3 @@ .. _Controlling files in the distribution:

will include any files that match the following glob patterns:
``LICEN[CS]E*``, ``COPYING*``, ``NOTICE*``, ``AUTHORS**``;
``LICENSE*``, ``LICENCE*``, ``COPYING*``, ``NOTICE*``, ``AUTHORS**``;
- ``pyproject.toml``;

@@ -42,5 +42,3 @@ - ``setup.cfg``;

by default in the distribution
(``.pyi`` and ``py.typed``, as specified in :pep:`561`),
as long as they are contained inside of a package directory
(for the time being there is no automatic support for top-level ``.pyi`` files).
(``.pyi`` and ``py.typed``, as specified in :pep:`561`).

@@ -47,0 +45,0 @@ *Please note however that this feature is* **EXPERIMENTAL** *and may change in

@@ -52,3 +52,3 @@ .. _pyproject.toml config:

keywords = ["one", "two"]
license = "BSD-3-Clause"
license = {text = "BSD-3-Clause"}
classifiers = [

@@ -74,10 +74,2 @@ "Framework :: Django",

.. important::
Support for
:external+PyPUG:ref:`project.license-files <license-files>`
and SPDX license expressions in
:external+PyPUG:ref:`project.license <license>` (:pep:`639`)
were introduced in version 77.0.0.
.. _setuptools-table:

@@ -112,4 +104,3 @@

------------------------- --------------------------- -------------------------
``license-files`` array of glob patterns **Deprecated** - use ``project.license-files`` instead. See
:external+PyPUG:ref:`Writing your pyproject.toml <license-files>`
``license-files`` array of glob patterns **Provisional** - likely to change with :pep:`639`
(by default: ``['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']``)

@@ -116,0 +107,0 @@ ``data-files`` table/inline-table **Discouraged** - check :doc:`/userguide/datafiles`.

@@ -202,3 +202,3 @@ ==========

able to automatically detect all :term:`packages <package>` and
:term:`namespaces <namespace package>`. However, complex projects might include
:term:`namespaces <namespace>`. However, complex projects might include
additional folders and supporting files that not necessarily should be

@@ -205,0 +205,0 @@ distributed (or that can confuse ``setuptools`` auto discovery algorithm).

@@ -23,3 +23,3 @@ [mypy]

# But our testing setup doesn't allow passing CLI arguments, so local devs have to set this manually.
# python_version = 3.9
# python_version = 3.8

@@ -62,3 +62,3 @@ exclude = (?x)(

[mypy-wheel.*]
follow_untyped_imports = True
ignore_missing_imports = True
# - The following are not marked as py.typed:

@@ -71,4 +71,4 @@ # - jaraco: Since mypy 1.12, the root name of the untyped namespace package gets called-out too

# - jaraco.text: https://github.com/jaraco/jaraco.text/issues/17
[mypy-jaraco,jaraco.develop.*,jaraco.envs,jaraco.packaging.*,jaraco.path,jaraco.text]
follow_untyped_imports = True
[mypy-jaraco,jaraco.develop,jaraco.envs,jaraco.packaging.*,jaraco.path,jaraco.text]
ignore_missing_imports = True

@@ -75,0 +75,0 @@ # Even when excluding a module, import issues can show up due to following import

@@ -5,3 +5,2 @@ from __future__ import annotations

import datetime
import inspect
import os

@@ -219,4 +218,4 @@ import plistlib

)
assert expected in actual, f'actual: {actual}'
assert actual.endswith(metadata_path), f'actual: {actual}'
assert expected in actual, 'actual: {}'.format(actual)
assert actual.endswith(metadata_path), 'actual: {}'.format(actual)

@@ -242,3 +241,3 @@

@pytest.mark.parametrize(
("suffix", "expected_filename", "expected_dist_type"),
'suffix, expected_filename, expected_dist_type',
[

@@ -259,7 +258,7 @@ ('egg-info', 'PKG-INFO', EggInfoDistribution),

"""
basename = f'foo.{suffix}'
basename = 'foo.{}'.format(suffix)
dist, dist_dir = make_distribution_no_version(tmpdir, basename)
expected_text = (
f"Missing 'Version:' header and/or {expected_filename} file at path: "
expected_text = ("Missing 'Version:' header and/or {} file at path: ").format(
expected_filename
)

@@ -384,3 +383,3 @@ metadata_path = os.path.join(dist_dir, expected_filename)

@pytest.mark.parametrize(
("unnormalized", "normalized"),
'unnormalized, normalized',
[

@@ -407,3 +406,3 @@ ('foo', 'foo'),

@pytest.mark.parametrize(
("unnormalized", "normalized"),
'unnormalized, normalized',
[

@@ -424,3 +423,3 @@ ('MiXeD/CasE', 'mixed/case'),

@pytest.mark.parametrize(
("unnormalized", "expected"),
'unnormalized, expected',
[

@@ -436,58 +435,1 @@ ('forward/slash', 'forward\\slash'),

assert result.endswith(expected)
class TestWorkdirRequire:
def fake_site_packages(self, tmp_path, monkeypatch, dist_files):
site_packages = tmp_path / "site-packages"
site_packages.mkdir()
for file, content in self.FILES.items():
path = site_packages / file
path.parent.mkdir(exist_ok=True, parents=True)
path.write_text(inspect.cleandoc(content), encoding="utf-8")
monkeypatch.setattr(sys, "path", [site_packages])
return os.fspath(site_packages)
FILES = {
"pkg1_mod-1.2.3.dist-info/METADATA": """
Metadata-Version: 2.4
Name: pkg1.mod
Version: 1.2.3
""",
"pkg2.mod-0.42.dist-info/METADATA": """
Metadata-Version: 2.1
Name: pkg2.mod
Version: 0.42
""",
"pkg3_mod.egg-info/PKG-INFO": """
Name: pkg3.mod
Version: 1.2.3.4
""",
"pkg4.mod.egg-info/PKG-INFO": """
Name: pkg4.mod
Version: 0.42.1
""",
}
@pytest.mark.parametrize(
("version", "requirement"),
[
("1.2.3", "pkg1.mod>=1"),
("0.42", "pkg2.mod>=0.4"),
("1.2.3.4", "pkg3.mod<=2"),
("0.42.1", "pkg4.mod>0.2,<1"),
],
)
def test_require_non_normalised_name(
self, tmp_path, monkeypatch, version, requirement
):
# https://github.com/pypa/setuptools/issues/4853
site_packages = self.fake_site_packages(tmp_path, monkeypatch, self.FILES)
ws = pkg_resources.WorkingSet([site_packages])
for req in [requirement, requirement.replace(".", "-")]:
[dist] = ws.require(req)
assert dist.version == version
assert os.path.samefile(
os.path.commonpath([dist.location, site_packages]), site_packages
)

@@ -35,3 +35,3 @@ import itertools

def __init__(self, *pairs) -> None:
def __init__(self, *pairs):
self.metadata = dict(pairs)

@@ -123,3 +123,3 @@

d = Distribution("/some/path")
assert d.py_version == f'{sys.version_info.major}.{sys.version_info.minor}'
assert d.py_version == '{}.{}'.format(*sys.version_info)
assert d.platform is None

@@ -698,10 +698,10 @@

def test_local_version(self):
parse_requirements('foo==1.0+org1')
(req,) = parse_requirements('foo==1.0+org1')
def test_spaces_between_multiple_versions(self):
parse_requirements('foo>=1.0, <3')
parse_requirements('foo >= 1.0, < 3')
(req,) = parse_requirements('foo>=1.0, <3')
(req,) = parse_requirements('foo >= 1.0, < 3')
@pytest.mark.parametrize(
("lower", "upper"),
['lower', 'upper'],
[

@@ -730,3 +730,3 @@ ('1.2-rc1', '1.2rc1'),

@pytest.mark.parametrize(
("lower", "upper"),
['lower', 'upper'],
[

@@ -733,0 +733,0 @@ ('2.1', '2.1.1'),

@@ -59,3 +59,3 @@ import functools

class FakeInstaller:
def __init__(self, installable_dists) -> None:
def __init__(self, installable_dists):
self._installable_dists = installable_dists

@@ -108,9 +108,5 @@

return pytest.mark.parametrize(
(
"installed_dists",
"installable_dists",
"requirements",
"replace_conflicting",
"resolved_dists_or_exception",
),
'installed_dists,installable_dists,'
'requirements,replace_conflicting,'
'resolved_dists_or_exception',
argvalues,

@@ -117,0 +113,0 @@ ids=idlist,

+15
-12

@@ -1,7 +0,6 @@

Metadata-Version: 2.4
Metadata-Version: 2.1
Name: setuptools
Version: 80.9.0
Version: 75.3.3
Summary: Easily download, build, install, upgrade, and uninstall Python packages
Author-email: Python Packaging Authority <distutils-sig@python.org>
License-Expression: MIT
Project-URL: Source, https://github.com/pypa/setuptools

@@ -13,2 +12,3 @@ Project-URL: Documentation, https://setuptools.pypa.io/

Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3

@@ -20,3 +20,3 @@ Classifier: Programming Language :: Python :: 3 :: Only

Classifier: Topic :: Utilities
Requires-Python: >=3.9
Requires-Python: >=3.8
Description-Content-Type: text/x-rst

@@ -29,6 +29,6 @@ License-File: LICENSE

Requires-Dist: pip>=19.1; extra == "test"
Requires-Dist: packaging>=24.2; extra == "test"
Requires-Dist: packaging>=23.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: jaraco.path>=3.2.0; extra == "test"
Requires-Dist: build[virtualenv]>=1.0.3; extra == "test"

@@ -45,2 +45,3 @@ Requires-Dist: filelock>=3.4.0; extra == "test"

Requires-Dist: jaraco.test>=5.5; extra == "test"
Requires-Dist: ruff<=0.7.1; extra == "test"
Provides-Extra: doc

@@ -64,5 +65,6 @@ Requires-Dist: sphinx>=3.5; extra == "doc"

Provides-Extra: core
Requires-Dist: packaging>=24.2; extra == "core"
Requires-Dist: packaging>=24; extra == "core"
Requires-Dist: more_itertools>=8.8; extra == "core"
Requires-Dist: jaraco.text>=3.7; extra == "core"
Requires-Dist: importlib_resources>=5.10.2; python_version < "3.9" and extra == "core"
Requires-Dist: importlib_metadata>=6; python_version < "3.10" and extra == "core"

@@ -72,3 +74,5 @@ Requires-Dist: tomli>=2.0.1; python_version < "3.11" and extra == "core"

Requires-Dist: platformdirs>=4.2.2; extra == "core"
Requires-Dist: jaraco.functools>=4; extra == "core"
Requires-Dist: jaraco.collections; extra == "core"
Requires-Dist: jaraco.functools; extra == "core"
Requires-Dist: packaging; extra == "core"
Requires-Dist: more_itertools; extra == "core"

@@ -78,3 +82,3 @@ Provides-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"
Requires-Dist: ruff>=0.5.2; sys_platform != "cygwin" and extra == "check"
Provides-Extra: cover

@@ -86,6 +90,5 @@ Requires-Dist: pytest-cov; extra == "cover"

Requires-Dist: pytest-mypy; extra == "type"
Requires-Dist: mypy==1.14.*; extra == "type"
Requires-Dist: mypy==1.12.*; 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

@@ -108,3 +111,3 @@ .. |pypi-version| image:: https://img.shields.io/pypi/v/setuptools.svg

.. |skeleton-badge| image:: https://img.shields.io/badge/skeleton-2025-informational
.. |skeleton-badge| image:: https://img.shields.io/badge/skeleton-2024-informational
:target: https://blog.jaraco.com/skeleton

@@ -111,0 +114,0 @@

[build-system]
requires = [
# "setuptools>=77",
# "setuptools_scm[toml]>=3.4.1",
# jaraco/skeleton#174
# "coherent.licensed",
]
requires = []
build-backend = "setuptools.build_meta"

@@ -13,3 +8,3 @@ backend-path = ["."]

name = "setuptools"
version = "80.9.0"
version = "75.3.3"
authors = [

@@ -23,2 +18,3 @@ { name = "Python Packaging Authority", email = "distutils-sig@python.org" },

"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",

@@ -31,7 +27,6 @@ "Programming Language :: Python :: 3 :: Only",

]
requires-python = ">=3.9"
license = "MIT"
keywords = ["CPAN PyPI distutils eggs package management"]
requires-python = ">=3.8"
dependencies = [
]
keywords = ["CPAN PyPI distutils eggs package management"]

@@ -52,6 +47,6 @@ [project.urls]

"pip>=19.1", # For proper file:// URLs support.
"packaging>=24.2",
"packaging>=23.2",
"jaraco.envs>=2.2",
"pytest-xdist>=3", # Dropped dependency on pytest-fork and py
"jaraco.path>=3.7.2", # Typing fixes
"jaraco.path>=3.2.0",
"build[virtualenv]>=1.0.3",

@@ -72,2 +67,5 @@ "filelock>=3.4.0",

"jaraco.test>=5.5", # py.typed
# temporarily pin ruff for backport bugfix (#4879)
"ruff <= 0.7.1",
]

@@ -103,5 +101,6 @@

core = [
"packaging>=24.2",
"packaging>=24",
"more_itertools>=8.8",
"jaraco.text>=3.7",
"importlib_resources>=5.10.2; python_version < '3.9'",
"importlib_metadata>=6; python_version < '3.10'",

@@ -115,3 +114,5 @@ "tomli>=2.0.1; python_version < '3.11'",

# for distutils
"jaraco.functools >= 4",
"jaraco.collections",
"jaraco.functools",
"packaging",
"more_itertools",

@@ -127,4 +128,4 @@ ]

# Removal of deprecated UP027, PT004 & PT005 astral-sh/ruff#14383
"ruff >= 0.8.0; sys_platform != 'cygwin'",
# workaround for businho/pytest-ruff#28
"ruff >= 0.5.2; sys_platform != 'cygwin'",
]

@@ -148,4 +149,4 @@

# until types-setuptools is removed from typeshed.
# For help with static-typing issues, or mypy update, ping @Avasam
"mypy==1.14.*",
# For help with static-typing issues, or mypy update, ping @Avasam
"mypy==1.12.*",
# Typing fixes in version newer than we require at runtime

@@ -225,6 +226,2 @@ "importlib_metadata>=7.0.2; python_version < '3.10'",

[tool.setuptools.exclude-package-data]
# Remove ruff.toml when installing vendored packages (#4652)
"*" = ["ruff.toml"]
[tool.distutils.sdist]

@@ -231,0 +228,0 @@ formats = "zip"

@@ -55,3 +55,2 @@ [pytest]

ignore:easy_install command is deprecated.
ignore:develop command is deprecated.

@@ -88,4 +87,4 @@ # https://github.com/pypa/setuptools/issues/2497

# suppress known deprecation pypa/setuptools#3085
ignore:pkg_resources is deprecated:UserWarning
# suppress known deprecation
ignore:pkg_resources is deprecated:DeprecationWarning

@@ -92,0 +91,0 @@ # Dependencies might not have been updated yet

@@ -17,3 +17,3 @@ .. |pypi-version| image:: https://img.shields.io/pypi/v/setuptools.svg

.. |skeleton-badge| image:: https://img.shields.io/badge/skeleton-2025-informational
.. |skeleton-badge| image:: https://img.shields.io/badge/skeleton-2024-informational
:target: https://blog.jaraco.com/skeleton

@@ -20,0 +20,0 @@

@@ -35,3 +35,3 @@ #!/usr/bin/env python

root = 'https://files.pythonhosted.org/packages/source'
name, _sep, _rest = pkg_filename.partition('-')
name, sep, rest = pkg_filename.partition('-')
parts = root, name[0], name, pkg_filename

@@ -38,0 +38,0 @@ return '/'.join(parts)

@@ -1,7 +0,6 @@

Metadata-Version: 2.4
Metadata-Version: 2.1
Name: setuptools
Version: 80.9.0
Version: 75.3.3
Summary: Easily download, build, install, upgrade, and uninstall Python packages
Author-email: Python Packaging Authority <distutils-sig@python.org>
License-Expression: MIT
Project-URL: Source, https://github.com/pypa/setuptools

@@ -13,2 +12,3 @@ Project-URL: Documentation, https://setuptools.pypa.io/

Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3

@@ -20,3 +20,3 @@ Classifier: Programming Language :: Python :: 3 :: Only

Classifier: Topic :: Utilities
Requires-Python: >=3.9
Requires-Python: >=3.8
Description-Content-Type: text/x-rst

@@ -29,6 +29,6 @@ License-File: LICENSE

Requires-Dist: pip>=19.1; extra == "test"
Requires-Dist: packaging>=24.2; extra == "test"
Requires-Dist: packaging>=23.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: jaraco.path>=3.2.0; extra == "test"
Requires-Dist: build[virtualenv]>=1.0.3; extra == "test"

@@ -45,2 +45,3 @@ Requires-Dist: filelock>=3.4.0; extra == "test"

Requires-Dist: jaraco.test>=5.5; extra == "test"
Requires-Dist: ruff<=0.7.1; extra == "test"
Provides-Extra: doc

@@ -64,5 +65,6 @@ Requires-Dist: sphinx>=3.5; extra == "doc"

Provides-Extra: core
Requires-Dist: packaging>=24.2; extra == "core"
Requires-Dist: packaging>=24; extra == "core"
Requires-Dist: more_itertools>=8.8; extra == "core"
Requires-Dist: jaraco.text>=3.7; extra == "core"
Requires-Dist: importlib_resources>=5.10.2; python_version < "3.9" and extra == "core"
Requires-Dist: importlib_metadata>=6; python_version < "3.10" and extra == "core"

@@ -72,3 +74,5 @@ Requires-Dist: tomli>=2.0.1; python_version < "3.11" and extra == "core"

Requires-Dist: platformdirs>=4.2.2; extra == "core"
Requires-Dist: jaraco.functools>=4; extra == "core"
Requires-Dist: jaraco.collections; extra == "core"
Requires-Dist: jaraco.functools; extra == "core"
Requires-Dist: packaging; extra == "core"
Requires-Dist: more_itertools; extra == "core"

@@ -78,3 +82,3 @@ Provides-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"
Requires-Dist: ruff>=0.5.2; sys_platform != "cygwin" and extra == "check"
Provides-Extra: cover

@@ -86,6 +90,5 @@ Requires-Dist: pytest-cov; extra == "cover"

Requires-Dist: pytest-mypy; extra == "type"
Requires-Dist: mypy==1.14.*; extra == "type"
Requires-Dist: mypy==1.12.*; 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

@@ -108,3 +111,3 @@ .. |pypi-version| image:: https://img.shields.io/pypi/v/setuptools.svg

.. |skeleton-badge| image:: https://img.shields.io/badge/skeleton-2025-informational
.. |skeleton-badge| image:: https://img.shields.io/badge/skeleton-2024-informational
:target: https://blog.jaraco.com/skeleton

@@ -111,0 +114,0 @@

@@ -9,6 +9,6 @@

pytest-ruff>=0.2.1
ruff>=0.8.0
ruff>=0.5.2
[core]
packaging>=24.2
packaging>=24
more_itertools>=8.8

@@ -18,3 +18,5 @@ jaraco.text>=3.7

platformdirs>=4.2.2
jaraco.functools>=4
jaraco.collections
jaraco.functools
packaging
more_itertools

@@ -28,2 +30,5 @@

[core:python_version < "3.9"]
importlib_resources>=5.10.2
[cover]

@@ -58,6 +63,6 @@ pytest-cov

pip>=19.1
packaging>=24.2
packaging>=23.2
jaraco.envs>=2.2
pytest-xdist>=3
jaraco.path>=3.7.2
jaraco.path>=3.2.0
build[virtualenv]>=1.0.3

@@ -72,2 +77,3 @@ filelock>=3.4.0

jaraco.test>=5.5
ruff<=0.7.1

@@ -82,3 +88,3 @@ [test:python_version >= "3.9" and sys_platform != "cygwin"]

pytest-mypy
mypy==1.14.*
mypy==1.12.*

@@ -85,0 +91,0 @@ [type:python_version < "3.10"]

@@ -61,3 +61,2 @@ LICENSE

docs/userguide/index.rst
docs/userguide/interfaces.rst
docs/userguide/miscellaneous.rst

@@ -90,3 +89,2 @@ docs/userguide/package_discovery.rst

setuptools/_core_metadata.py
setuptools/_discovery.py
setuptools/_entry_points.py

@@ -99,5 +97,2 @@ setuptools/_imp.py

setuptools/_reqs.py
setuptools/_scripts.py
setuptools/_shutil.py
setuptools/_static.py
setuptools/archive_util.py

@@ -126,2 +121,4 @@ setuptools/build_meta.py

setuptools/namespaces.py
setuptools/package_index.py
setuptools/sandbox.py
setuptools/script (dev).tmpl

@@ -189,15 +186,4 @@ setuptools/script.tmpl

setuptools/_distutils/compat/__init__.py
setuptools/_distutils/compat/numpy.py
setuptools/_distutils/compat/py38.py
setuptools/_distutils/compat/py39.py
setuptools/_distutils/compilers/C/base.py
setuptools/_distutils/compilers/C/cygwin.py
setuptools/_distutils/compilers/C/errors.py
setuptools/_distutils/compilers/C/msvc.py
setuptools/_distutils/compilers/C/unix.py
setuptools/_distutils/compilers/C/zos.py
setuptools/_distutils/compilers/C/tests/test_base.py
setuptools/_distutils/compilers/C/tests/test_cygwin.py
setuptools/_distutils/compilers/C/tests/test_mingw.py
setuptools/_distutils/compilers/C/tests/test_msvc.py
setuptools/_distutils/compilers/C/tests/test_unix.py
setuptools/_distutils/tests/__init__.py

@@ -214,2 +200,3 @@ setuptools/_distutils/tests/support.py

setuptools/_distutils/tests/test_build_scripts.py
setuptools/_distutils/tests/test_ccompiler.py
setuptools/_distutils/tests/test_check.py

@@ -220,2 +207,3 @@ setuptools/_distutils/tests/test_clean.py

setuptools/_distutils/tests/test_core.py
setuptools/_distutils/tests/test_cygwinccompiler.py
setuptools/_distutils/tests/test_dir_util.py

@@ -232,3 +220,5 @@ setuptools/_distutils/tests/test_dist.py

setuptools/_distutils/tests/test_log.py
setuptools/_distutils/tests/test_mingwccompiler.py
setuptools/_distutils/tests/test_modified.py
setuptools/_distutils/tests/test_msvccompiler.py
setuptools/_distutils/tests/test_sdist.py

@@ -238,2 +228,3 @@ setuptools/_distutils/tests/test_spawn.py

setuptools/_distutils/tests/test_text_file.py
setuptools/_distutils/tests/test_unixccompiler.py
setuptools/_distutils/tests/test_util.py

@@ -244,3 +235,3 @@ setuptools/_distutils/tests/test_version.py

setuptools/_distutils/tests/compat/__init__.py
setuptools/_distutils/tests/compat/py39.py
setuptools/_distutils/tests/compat/py38.py
setuptools/_vendor/ruff.toml

@@ -292,2 +283,56 @@ setuptools/_vendor/typing_extensions.py

setuptools/_vendor/importlib_metadata/compat/py39.py
setuptools/_vendor/importlib_resources/__init__.py
setuptools/_vendor/importlib_resources/_adapters.py
setuptools/_vendor/importlib_resources/_common.py
setuptools/_vendor/importlib_resources/_itertools.py
setuptools/_vendor/importlib_resources/abc.py
setuptools/_vendor/importlib_resources/functional.py
setuptools/_vendor/importlib_resources/py.typed
setuptools/_vendor/importlib_resources/readers.py
setuptools/_vendor/importlib_resources/simple.py
setuptools/_vendor/importlib_resources-6.4.0.dist-info/INSTALLER
setuptools/_vendor/importlib_resources-6.4.0.dist-info/LICENSE
setuptools/_vendor/importlib_resources-6.4.0.dist-info/METADATA
setuptools/_vendor/importlib_resources-6.4.0.dist-info/RECORD
setuptools/_vendor/importlib_resources-6.4.0.dist-info/REQUESTED
setuptools/_vendor/importlib_resources-6.4.0.dist-info/WHEEL
setuptools/_vendor/importlib_resources-6.4.0.dist-info/top_level.txt
setuptools/_vendor/importlib_resources/compat/__init__.py
setuptools/_vendor/importlib_resources/compat/py38.py
setuptools/_vendor/importlib_resources/compat/py39.py
setuptools/_vendor/importlib_resources/future/__init__.py
setuptools/_vendor/importlib_resources/future/adapters.py
setuptools/_vendor/importlib_resources/tests/__init__.py
setuptools/_vendor/importlib_resources/tests/_path.py
setuptools/_vendor/importlib_resources/tests/test_compatibilty_files.py
setuptools/_vendor/importlib_resources/tests/test_contents.py
setuptools/_vendor/importlib_resources/tests/test_custom.py
setuptools/_vendor/importlib_resources/tests/test_files.py
setuptools/_vendor/importlib_resources/tests/test_functional.py
setuptools/_vendor/importlib_resources/tests/test_open.py
setuptools/_vendor/importlib_resources/tests/test_path.py
setuptools/_vendor/importlib_resources/tests/test_read.py
setuptools/_vendor/importlib_resources/tests/test_reader.py
setuptools/_vendor/importlib_resources/tests/test_resource.py
setuptools/_vendor/importlib_resources/tests/util.py
setuptools/_vendor/importlib_resources/tests/zip.py
setuptools/_vendor/importlib_resources/tests/compat/__init__.py
setuptools/_vendor/importlib_resources/tests/compat/py312.py
setuptools/_vendor/importlib_resources/tests/compat/py39.py
setuptools/_vendor/importlib_resources/tests/data01/__init__.py
setuptools/_vendor/importlib_resources/tests/data01/binary.file
setuptools/_vendor/importlib_resources/tests/data01/utf-16.file
setuptools/_vendor/importlib_resources/tests/data01/utf-8.file
setuptools/_vendor/importlib_resources/tests/data01/subdirectory/__init__.py
setuptools/_vendor/importlib_resources/tests/data01/subdirectory/binary.file
setuptools/_vendor/importlib_resources/tests/data02/__init__.py
setuptools/_vendor/importlib_resources/tests/data02/one/__init__.py
setuptools/_vendor/importlib_resources/tests/data02/one/resource1.txt
setuptools/_vendor/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt
setuptools/_vendor/importlib_resources/tests/data02/two/__init__.py
setuptools/_vendor/importlib_resources/tests/data02/two/resource2.txt
setuptools/_vendor/importlib_resources/tests/namespacedata01/binary.file
setuptools/_vendor/importlib_resources/tests/namespacedata01/utf-16.file
setuptools/_vendor/importlib_resources/tests/namespacedata01/utf-8.file
setuptools/_vendor/importlib_resources/tests/namespacedata01/subdirectory/binary.file
setuptools/_vendor/inflect/__init__.py

@@ -370,12 +415,10 @@ setuptools/_vendor/inflect/py.typed

setuptools/_vendor/packaging/version.py
setuptools/_vendor/packaging-24.2.dist-info/INSTALLER
setuptools/_vendor/packaging-24.2.dist-info/LICENSE
setuptools/_vendor/packaging-24.2.dist-info/LICENSE.APACHE
setuptools/_vendor/packaging-24.2.dist-info/LICENSE.BSD
setuptools/_vendor/packaging-24.2.dist-info/METADATA
setuptools/_vendor/packaging-24.2.dist-info/RECORD
setuptools/_vendor/packaging-24.2.dist-info/REQUESTED
setuptools/_vendor/packaging-24.2.dist-info/WHEEL
setuptools/_vendor/packaging/licenses/__init__.py
setuptools/_vendor/packaging/licenses/_spdx.py
setuptools/_vendor/packaging-24.1.dist-info/INSTALLER
setuptools/_vendor/packaging-24.1.dist-info/LICENSE
setuptools/_vendor/packaging-24.1.dist-info/LICENSE.APACHE
setuptools/_vendor/packaging-24.1.dist-info/LICENSE.BSD
setuptools/_vendor/packaging-24.1.dist-info/METADATA
setuptools/_vendor/packaging-24.1.dist-info/RECORD
setuptools/_vendor/packaging-24.1.dist-info/REQUESTED
setuptools/_vendor/packaging-24.1.dist-info/WHEEL
setuptools/_vendor/platformdirs/__init__.py

@@ -435,3 +478,2 @@ setuptools/_vendor/platformdirs/__main__.py

setuptools/_vendor/wheel/__main__.py
setuptools/_vendor/wheel/_bdist_wheel.py
setuptools/_vendor/wheel/_setuptools_logging.py

@@ -443,9 +485,9 @@ setuptools/_vendor/wheel/bdist_wheel.py

setuptools/_vendor/wheel/wheelfile.py
setuptools/_vendor/wheel-0.45.1.dist-info/INSTALLER
setuptools/_vendor/wheel-0.45.1.dist-info/LICENSE.txt
setuptools/_vendor/wheel-0.45.1.dist-info/METADATA
setuptools/_vendor/wheel-0.45.1.dist-info/RECORD
setuptools/_vendor/wheel-0.45.1.dist-info/REQUESTED
setuptools/_vendor/wheel-0.45.1.dist-info/WHEEL
setuptools/_vendor/wheel-0.45.1.dist-info/entry_points.txt
setuptools/_vendor/wheel-0.43.0.dist-info/INSTALLER
setuptools/_vendor/wheel-0.43.0.dist-info/LICENSE.txt
setuptools/_vendor/wheel-0.43.0.dist-info/METADATA
setuptools/_vendor/wheel-0.43.0.dist-info/RECORD
setuptools/_vendor/wheel-0.43.0.dist-info/REQUESTED
setuptools/_vendor/wheel-0.43.0.dist-info/WHEEL
setuptools/_vendor/wheel-0.43.0.dist-info/entry_points.txt
setuptools/_vendor/wheel/cli/__init__.py

@@ -458,5 +500,2 @@ setuptools/_vendor/wheel/cli/convert.py

setuptools/_vendor/wheel/vendored/vendor.txt
setuptools/_vendor/wheel/vendored/packaging/LICENSE
setuptools/_vendor/wheel/vendored/packaging/LICENSE.APACHE
setuptools/_vendor/wheel/vendored/packaging/LICENSE.BSD
setuptools/_vendor/wheel/vendored/packaging/__init__.py

@@ -538,2 +577,3 @@ setuptools/_vendor/wheel/vendored/packaging/_elffile.py

setuptools/tests/script-with-bom.py
setuptools/tests/server.py
setuptools/tests/test_archive_util.py

@@ -555,2 +595,3 @@ setuptools/tests/test_bdist_deprecations.py

setuptools/tests/test_distutils_adoption.py
setuptools/tests/test_easy_install.py
setuptools/tests/test_editable_install.py

@@ -566,7 +607,7 @@ setuptools/tests/test_egg_info.py

setuptools/tests/test_namespaces.py
setuptools/tests/test_scripts.py
setuptools/tests/test_packageindex.py
setuptools/tests/test_sandbox.py
setuptools/tests/test_sdist.py
setuptools/tests/test_setopt.py
setuptools/tests/test_setuptools.py
setuptools/tests/test_shutil_wrapper.py
setuptools/tests/test_unicode_utils.py

@@ -594,3 +635,2 @@ setuptools/tests/test_virtualenv.py

setuptools/tests/integration/helpers.py
setuptools/tests/integration/test_pbr.py
setuptools/tests/integration/test_pip_install_sdist.py

@@ -597,0 +637,0 @@ tools/build_launchers.py

@@ -12,2 +12,3 @@ """Extensions to the 'distutils' for large or complex distributions"""

import os
import re
import sys

@@ -33,2 +34,3 @@ from abc import abstractmethod

import distutils.core
from distutils.errors import DistutilsOptionError

@@ -63,3 +65,3 @@ __all__ = [

def __init__(self, attrs: Mapping[str, object]) -> None:
def __init__(self, attrs: Mapping[str, object]):
_incl = 'dependency_links', 'setup_requires'

@@ -74,3 +76,3 @@ filtered = {k: attrs[k] for k in set(_incl) & set(attrs)}

try:
cfg, _toml = super()._split_standard_project_metadata(filenames)
cfg, toml = super()._split_standard_project_metadata(filenames)
except Exception:

@@ -172,3 +174,3 @@ return filenames, ()

def __init__(self, dist: Distribution, **kw) -> None:
def __init__(self, dist: Distribution, **kw):
"""

@@ -181,2 +183,40 @@ Construct the command for dist, updating

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(
"'%s' must be a %s (got `%s`)" % (option, what, val)
)
return val
def ensure_string_list(self, option: str):
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(
"'%s' must be a list of strings (got %r)" % (option, val)
)
@overload

@@ -195,3 +235,3 @@ def reinitialize_command(

vars(cmd).update(kw)
return cmd # pyright: ignore[reportReturnType] # pypa/distutils#307
return cmd

@@ -198,0 +238,0 @@ @abstractmethod

@@ -22,3 +22,2 @@ """

from . import _normalization, _reqs
from ._static import is_static
from .warnings import SetuptoolsDeprecationWarning

@@ -32,3 +31,3 @@

if mv is None:
mv = Version('2.4')
mv = Version('2.1')
self.metadata_version = mv

@@ -93,3 +92,2 @@ return mv

self.license = _read_field_unescaped_from_msg(msg, 'license')
self.license_expression = _read_field_unescaped_from_msg(msg, 'license-expression')

@@ -158,3 +156,3 @@ self.long_description = _read_field_unescaped_from_msg(msg, 'description')

def write_field(key, value):
file.write(f"{key}: {value}\n")
file.write("%s: %s\n" % (key, value))

@@ -183,9 +181,8 @@ write_field('Metadata-Version', str(version))

if license_expression := self.license_expression:
write_field('License-Expression', license_expression)
elif license := self.get_license():
license = self.get_license()
if license:
write_field('License', rfc822_escape(license))
for label, url in self.project_urls.items():
write_field('Project-URL', f'{label}, {url}')
for project_url in self.project_urls.items():
write_field('Project-URL', '%s, %s' % project_url)

@@ -215,13 +212,8 @@ keywords = ','.join(self.get_keywords())

safe_license_files = map(_safe_license_file, self.license_files or [])
self._write_list(file, 'License-File', safe_license_files)
self._write_list(file, 'License-File', self.license_files or [])
_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}")
file.write("\n%s" % long_description)
if not long_description.endswith("\n"):

@@ -301,44 +293,1 @@ file.write("\n")

)
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
}
"""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

@@ -15,10 +11,3 @@ from jaraco.functools import splat

_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):

@@ -30,6 +19,3 @@ return not os.path.exists(target) or (

def newer(
source: str | bytes | os.PathLike[str] | os.PathLike[bytes],
target: str | bytes | os.PathLike[str] | os.PathLike[bytes],
) -> bool:
def newer(source, target):
"""

@@ -44,3 +30,3 @@ Is source modified more recently than target.

if not os.path.exists(source):
raise DistutilsFileError(f"file {os.path.abspath(source)!r} does not exist")
raise DistutilsFileError(f"file '{os.path.abspath(source)}' does not exist")

@@ -50,7 +36,3 @@ return _newer(source, target)

def newer_pairwise(
sources: Iterable[_SourcesT],
targets: Iterable[_TargetsT],
newer: Callable[[_SourcesT, _TargetsT], bool] = newer,
) -> tuple[list[_SourcesT], list[_TargetsT]]:
def newer_pairwise(sources, targets, newer=newer):
"""

@@ -68,7 +50,3 @@ Filter filenames where sources are newer than targets.

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:
def newer_group(sources, target, missing='error'):
"""

@@ -75,0 +53,0 @@ Is target out-of-date with respect to any file in sources.

@@ -0,16 +1,604 @@

"""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
import contextlib
import os
import subprocess
import unittest.mock as mock
import warnings
from .compilers.C import msvc
with contextlib.suppress(ImportError):
import winreg
__all__ = ["MSVCCompiler"]
from itertools import count
MSVCCompiler = msvc.Compiler
from ._log import log
from .ccompiler import CCompiler, gen_lib_options
from .errors import (
CompileError,
DistutilsExecError,
DistutilsPlatformError,
LibError,
LinkError,
)
from .util import get_host_platform, get_platform
def __getattr__(name):
if name == '_get_vc_env':
warnings.warn(
"_get_vc_env is private; find an alternative (pypa/distutils#340)"
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,
)
return msvc._get_vc_env
raise AttributeError(name)
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 MSVCCompiler(CCompiler):
"""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):
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=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 = {
(CCompiler.EXECUTABLE, None): self.ldflags_exe,
(CCompiler.EXECUTABLE, False): self.ldflags_exe,
(CCompiler.EXECUTABLE, True): self.ldflags_exe_debug,
(CCompiler.SHARED_OBJECT, None): self.ldflags_shared,
(CCompiler.SHARED_OBJECT, False): self.ldflags_shared,
(CCompiler.SHARED_OBJECT, True): self.ldflags_shared_debug,
(CCompiler.SHARED_LIBRARY, None): self.ldflags_static,
(CCompiler.SHARED_LIBRARY, False): self.ldflags_static,
(CCompiler.SHARED_LIBRARY, True): self.ldflags_static_debug,
}
self.initialized = True
# -- Worker methods ------------------------------------------------
@property
def out_extensions(self):
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 = "/Tc" + src
elif ext in self._cpp_extensions:
input_opt = "/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, output_libname, output_dir=None, debug=False, target_lang=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,
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,
):
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

@@ -6,6 +6,3 @@ """distutils.archive_util

from __future__ import annotations
import os
from typing import Literal, overload

@@ -61,10 +58,10 @@ try:

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:
base_name,
base_dir,
compress="gzip",
verbose=False,
dry_run=False,
owner=None,
group=None,
):
"""Create a (possibly compressed) tar file from all the files under

@@ -130,8 +127,3 @@ 'base_dir'.

def make_zipfile( # noqa: C901
base_name: str,
base_dir: str | os.PathLike[str],
verbose: bool = False,
dry_run: bool = False,
) -> str:
def make_zipfile(base_name, base_dir, verbose=False, dry_run=False): # noqa: C901
"""Create a zip file from all the files under 'base_dir'.

@@ -218,34 +210,12 @@

@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:
base_name,
format,
root_dir=None,
base_dir=None,
verbose=False,
dry_run=False,
owner=None,
group=None,
):
"""Create an archive file (eg. zip or tar).

@@ -252,0 +222,0 @@

@@ -1,26 +0,1256 @@

from .compat.numpy import ( # noqa: F401
_default_compilers,
compiler_class,
"""distutils.ccompiler
Contains CCompiler, an abstract base class that defines the interface
for the Distutils compiler abstraction model."""
import os
import re
import sys
import types
import warnings
from more_itertools import always_iterable
from ._log import log
from ._modified import newer_group
from .dir_util import mkpath
from .errors import (
CompileError,
DistutilsModuleError,
DistutilsPlatformError,
LinkError,
UnknownFileError,
)
from .compilers.C import base
from .compilers.C.base import (
gen_lib_options,
gen_preprocess_options,
get_default_compiler,
new_compiler,
show_compilers,
from .file_util import move_file
from .spawn import spawn
from .util import execute, is_mingw, split_quoted
class CCompiler:
"""Abstract base class to define the interface that must be implemented
by real compiler classes. Also has some utility methods used by
several compiler classes.
The basic idea behind a compiler abstraction class is that each
instance can be used for all the compile/link steps in building a
single project. Thus, attributes common to all of those compile and
link steps -- include directories, macros to define, libraries to link
against, etc. -- are attributes of the compiler instance. To allow for
variability in how individual files are treated, most of those
attributes may be varied on a per-compilation or per-link basis.
"""
# 'compiler_type' is a class attribute that identifies this class. It
# keeps code that wants to know what kind of compiler it's dealing with
# from having to import all possible compiler classes just to do an
# 'isinstance'. In concrete CCompiler subclasses, 'compiler_type'
# should really, really be one of the keys of the 'compiler_class'
# dictionary (see below -- used by the 'new_compiler()' factory
# function) -- authors of new compiler interface classes are
# responsible for updating 'compiler_class'!
compiler_type = None
# XXX things not handled by this compiler abstraction model:
# * client can't provide additional options for a compiler,
# e.g. warning, optimization, debugging flags. Perhaps this
# should be the domain of concrete compiler abstraction classes
# (UnixCCompiler, MSVCCompiler, etc.) -- or perhaps the base
# class should have methods for the common ones.
# * can't completely override the include or library searchg
# path, ie. no "cc -I -Idir1 -Idir2" or "cc -L -Ldir1 -Ldir2".
# I'm not sure how widely supported this is even by Unix
# compilers, much less on other platforms. And I'm even less
# sure how useful it is; maybe for cross-compiling, but
# support for that is a ways off. (And anyways, cross
# compilers probably have a dedicated binary with the
# right paths compiled in. I hope.)
# * can't do really freaky things with the library list/library
# dirs, e.g. "-Ldir1 -lfoo -Ldir2 -lfoo" to link against
# different versions of libfoo.a in different locations. I
# think this is useless without the ability to null out the
# library search path anyways.
# Subclasses that rely on the standard filename generation methods
# implemented below should override these; see the comment near
# those methods ('object_filenames()' et. al.) for details:
src_extensions = None # list of strings
obj_extension = None # string
static_lib_extension = None
shared_lib_extension = None # string
static_lib_format = None # format string
shared_lib_format = None # prob. same as static_lib_format
exe_extension = None # string
# Default language settings. language_map is used to detect a source
# file or Extension target language, checking source filenames.
# language_order is used to detect the language precedence, when deciding
# what language to use when mixing source types. For example, if some
# extension has two files with ".c" extension, and one with ".cpp", it
# is still linked as c++.
language_map = {
".c": "c",
".cc": "c++",
".cpp": "c++",
".cxx": "c++",
".m": "objc",
}
language_order = ["c++", "objc", "c"]
include_dirs = []
"""
include dirs specific to this compiler class
"""
library_dirs = []
"""
library dirs specific to this compiler class
"""
def __init__(self, verbose=False, dry_run=False, force=False):
self.dry_run = dry_run
self.force = force
self.verbose = verbose
# 'output_dir': a common output directory for object, library,
# shared object, and shared library files
self.output_dir = None
# 'macros': a list of macro definitions (or undefinitions). A
# macro definition is a 2-tuple (name, value), where the value is
# either a string or None (no explicit value). A macro
# undefinition is a 1-tuple (name,).
self.macros = []
# 'include_dirs': a list of directories to search for include files
self.include_dirs = []
# 'libraries': a list of libraries to include in any link
# (library names, not filenames: eg. "foo" not "libfoo.a")
self.libraries = []
# 'library_dirs': a list of directories to search for libraries
self.library_dirs = []
# 'runtime_library_dirs': a list of directories to search for
# shared libraries/objects at runtime
self.runtime_library_dirs = []
# 'objects': a list of object files (or similar, such as explicitly
# named library files) to include on any link
self.objects = []
for key in self.executables.keys():
self.set_executable(key, self.executables[key])
def set_executables(self, **kwargs):
"""Define the executables (and options for them) that will be run
to perform the various stages of compilation. The exact set of
executables that may be specified here depends on the compiler
class (via the 'executables' class attribute), but most will have:
compiler the C/C++ compiler
linker_so linker used to create shared objects and libraries
linker_exe linker used to create binary executables
archiver static library creator
On platforms with a command-line (Unix, DOS/Windows), each of these
is a string that will be split into executable name and (optional)
list of arguments. (Splitting the string is done similarly to how
Unix shells operate: words are delimited by spaces, but quotes and
backslashes can override this. See
'distutils.util.split_quoted()'.)
"""
# Note that some CCompiler implementation classes will define class
# attributes 'cpp', 'cc', etc. with hard-coded executable names;
# this is appropriate when a compiler class is for exactly one
# compiler/OS combination (eg. MSVCCompiler). Other compiler
# classes (UnixCCompiler, in particular) are driven by information
# discovered at run-time, since there are many different ways to do
# basically the same things with Unix C compilers.
for key in kwargs:
if key not in self.executables:
raise ValueError(
f"unknown executable '{key}' for class {self.__class__.__name__}"
)
self.set_executable(key, kwargs[key])
def set_executable(self, key, value):
if isinstance(value, str):
setattr(self, key, split_quoted(value))
else:
setattr(self, key, value)
def _find_macro(self, name):
i = 0
for defn in self.macros:
if defn[0] == name:
return i
i += 1
return None
def _check_macro_definitions(self, definitions):
"""Ensure that every element of 'definitions' is valid."""
for defn in definitions:
self._check_macro_definition(*defn)
def _check_macro_definition(self, defn):
"""
Raise a TypeError if defn is not valid.
A valid definition is either a (name, value) 2-tuple or a (name,) tuple.
"""
if not isinstance(defn, tuple) or not self._is_valid_macro(*defn):
raise TypeError(
f"invalid macro definition '{defn}': "
"must be tuple (string,), (string, string), or (string, None)"
)
@staticmethod
def _is_valid_macro(name, value=None):
"""
A valid macro is a ``name : str`` and a ``value : str | None``.
"""
return isinstance(name, str) and isinstance(value, (str, types.NoneType))
# -- Bookkeeping methods -------------------------------------------
def define_macro(self, name, value=None):
"""Define a preprocessor macro for all compilations driven by this
compiler object. The optional parameter 'value' should be a
string; if it is not supplied, then the macro will be defined
without an explicit value and the exact outcome depends on the
compiler used (XXX true? does ANSI say anything about this?)
"""
# Delete from the list of macro definitions/undefinitions if
# already there (so that this one will take precedence).
i = self._find_macro(name)
if i is not None:
del self.macros[i]
self.macros.append((name, value))
def undefine_macro(self, name):
"""Undefine a preprocessor macro for all compilations driven by
this compiler object. If the same macro is defined by
'define_macro()' and undefined by 'undefine_macro()' the last call
takes precedence (including multiple redefinitions or
undefinitions). If the macro is redefined/undefined on a
per-compilation basis (ie. in the call to 'compile()'), then that
takes precedence.
"""
# Delete from the list of macro definitions/undefinitions if
# already there (so that this one will take precedence).
i = self._find_macro(name)
if i is not None:
del self.macros[i]
undefn = (name,)
self.macros.append(undefn)
def add_include_dir(self, dir):
"""Add 'dir' to the list of directories that will be searched for
header files. The compiler is instructed to search directories in
the order in which they are supplied by successive calls to
'add_include_dir()'.
"""
self.include_dirs.append(dir)
def set_include_dirs(self, dirs):
"""Set the list of directories that will be searched to 'dirs' (a
list of strings). Overrides any preceding calls to
'add_include_dir()'; subsequence calls to 'add_include_dir()' add
to the list passed to 'set_include_dirs()'. This does not affect
any list of standard include directories that the compiler may
search by default.
"""
self.include_dirs = dirs[:]
def add_library(self, libname):
"""Add 'libname' to the list of libraries that will be included in
all links driven by this compiler object. Note that 'libname'
should *not* be the name of a file containing a library, but the
name of the library itself: the actual filename will be inferred by
the linker, the compiler, or the compiler class (depending on the
platform).
The linker will be instructed to link against libraries in the
order they were supplied to 'add_library()' and/or
'set_libraries()'. It is perfectly valid to duplicate library
names; the linker will be instructed to link against libraries as
many times as they are mentioned.
"""
self.libraries.append(libname)
def set_libraries(self, libnames):
"""Set the list of libraries to be included in all links driven by
this compiler object to 'libnames' (a list of strings). This does
not affect any standard system libraries that the linker may
include by default.
"""
self.libraries = libnames[:]
def add_library_dir(self, dir):
"""Add 'dir' to the list of directories that will be searched for
libraries specified to 'add_library()' and 'set_libraries()'. The
linker will be instructed to search for libraries in the order they
are supplied to 'add_library_dir()' and/or 'set_library_dirs()'.
"""
self.library_dirs.append(dir)
def set_library_dirs(self, dirs):
"""Set the list of library search directories to 'dirs' (a list of
strings). This does not affect any standard library search path
that the linker may search by default.
"""
self.library_dirs = dirs[:]
def add_runtime_library_dir(self, dir):
"""Add 'dir' to the list of directories that will be searched for
shared libraries at runtime.
"""
self.runtime_library_dirs.append(dir)
def set_runtime_library_dirs(self, dirs):
"""Set the list of directories to search for shared libraries at
runtime to 'dirs' (a list of strings). This does not affect any
standard search path that the runtime linker may search by
default.
"""
self.runtime_library_dirs = dirs[:]
def add_link_object(self, object):
"""Add 'object' to the list of object files (or analogues, such as
explicitly named library files or the output of "resource
compilers") to be included in every link driven by this compiler
object.
"""
self.objects.append(object)
def set_link_objects(self, objects):
"""Set the list of object files (or analogues) to be included in
every link to 'objects'. This does not affect any standard object
files that the linker may include by default (such as system
libraries).
"""
self.objects = objects[:]
# -- Private utility methods --------------------------------------
# (here for the convenience of subclasses)
# Helper method to prep compiler in subclass compile() methods
def _setup_compile(self, outdir, macros, incdirs, sources, depends, extra):
"""Process arguments and decide which source files to compile."""
outdir, macros, incdirs = self._fix_compile_args(outdir, macros, incdirs)
if extra is None:
extra = []
# Get the list of expected output (object) files
objects = self.object_filenames(sources, strip_dir=False, output_dir=outdir)
assert len(objects) == len(sources)
pp_opts = gen_preprocess_options(macros, incdirs)
build = {}
for i in range(len(sources)):
src = sources[i]
obj = objects[i]
ext = os.path.splitext(src)[1]
self.mkpath(os.path.dirname(obj))
build[obj] = (src, ext)
return macros, objects, extra, pp_opts, build
def _get_cc_args(self, pp_opts, debug, before):
# works for unixccompiler, cygwinccompiler
cc_args = pp_opts + ['-c']
if debug:
cc_args[:0] = ['-g']
if before:
cc_args[:0] = before
return cc_args
def _fix_compile_args(self, output_dir, macros, include_dirs):
"""Typecheck and fix-up some of the arguments to the 'compile()'
method, and return fixed-up values. Specifically: if 'output_dir'
is None, replaces it with 'self.output_dir'; ensures that 'macros'
is a list, and augments it with 'self.macros'; ensures that
'include_dirs' is a list, and augments it with 'self.include_dirs'.
Guarantees that the returned values are of the correct type,
i.e. for 'output_dir' either string or None, and for 'macros' and
'include_dirs' either list or None.
"""
if output_dir is None:
output_dir = self.output_dir
elif not isinstance(output_dir, str):
raise TypeError("'output_dir' must be a string or None")
if macros is None:
macros = list(self.macros)
elif isinstance(macros, list):
macros = macros + (self.macros or [])
else:
raise TypeError("'macros' (if supplied) must be a list of tuples")
if include_dirs is None:
include_dirs = list(self.include_dirs)
elif isinstance(include_dirs, (list, tuple)):
include_dirs = list(include_dirs) + (self.include_dirs or [])
else:
raise TypeError("'include_dirs' (if supplied) must be a list of strings")
# add include dirs for class
include_dirs += self.__class__.include_dirs
return output_dir, macros, include_dirs
def _prep_compile(self, sources, output_dir, depends=None):
"""Decide which source files must be recompiled.
Determine the list of object files corresponding to 'sources',
and figure out which ones really need to be recompiled.
Return a list of all object files and a dictionary telling
which source files can be skipped.
"""
# Get the list of expected output (object) files
objects = self.object_filenames(sources, output_dir=output_dir)
assert len(objects) == len(sources)
# Return an empty dict for the "which source files can be skipped"
# return value to preserve API compatibility.
return objects, {}
def _fix_object_args(self, objects, output_dir):
"""Typecheck and fix up some arguments supplied to various methods.
Specifically: ensure that 'objects' is a list; if output_dir is
None, replace with self.output_dir. Return fixed versions of
'objects' and 'output_dir'.
"""
if not isinstance(objects, (list, tuple)):
raise TypeError("'objects' must be a list or tuple of strings")
objects = list(objects)
if output_dir is None:
output_dir = self.output_dir
elif not isinstance(output_dir, str):
raise TypeError("'output_dir' must be a string or None")
return (objects, output_dir)
def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs):
"""Typecheck and fix up some of the arguments supplied to the
'link_*' methods. Specifically: ensure that all arguments are
lists, and augment them with their permanent versions
(eg. 'self.libraries' augments 'libraries'). Return a tuple with
fixed versions of all arguments.
"""
if libraries is None:
libraries = list(self.libraries)
elif isinstance(libraries, (list, tuple)):
libraries = list(libraries) + (self.libraries or [])
else:
raise TypeError("'libraries' (if supplied) must be a list of strings")
if library_dirs is None:
library_dirs = list(self.library_dirs)
elif isinstance(library_dirs, (list, tuple)):
library_dirs = list(library_dirs) + (self.library_dirs or [])
else:
raise TypeError("'library_dirs' (if supplied) must be a list of strings")
# add library dirs for class
library_dirs += self.__class__.library_dirs
if runtime_library_dirs is None:
runtime_library_dirs = list(self.runtime_library_dirs)
elif isinstance(runtime_library_dirs, (list, tuple)):
runtime_library_dirs = list(runtime_library_dirs) + (
self.runtime_library_dirs or []
)
else:
raise TypeError(
"'runtime_library_dirs' (if supplied) must be a list of strings"
)
return (libraries, library_dirs, runtime_library_dirs)
def _need_link(self, objects, output_file):
"""Return true if we need to relink the files listed in 'objects'
to recreate 'output_file'.
"""
if self.force:
return True
else:
if self.dry_run:
newer = newer_group(objects, output_file, missing='newer')
else:
newer = newer_group(objects, output_file)
return newer
def detect_language(self, sources):
"""Detect the language of a given file, or list of files. Uses
language_map, and language_order to do the job.
"""
if not isinstance(sources, list):
sources = [sources]
lang = None
index = len(self.language_order)
for source in sources:
base, ext = os.path.splitext(source)
extlang = self.language_map.get(ext)
try:
extindex = self.language_order.index(extlang)
if extindex < index:
lang = extlang
index = extindex
except ValueError:
pass
return lang
# -- Worker methods ------------------------------------------------
# (must be implemented by subclasses)
def preprocess(
self,
source,
output_file=None,
macros=None,
include_dirs=None,
extra_preargs=None,
extra_postargs=None,
):
"""Preprocess a single C/C++ source file, named in 'source'.
Output will be written to file named 'output_file', or stdout if
'output_file' not supplied. 'macros' is a list of macro
definitions as for 'compile()', which will augment the macros set
with 'define_macro()' and 'undefine_macro()'. 'include_dirs' is a
list of directory names that will be added to the default list.
Raises PreprocessError on failure.
"""
pass
def compile(
self,
sources,
output_dir=None,
macros=None,
include_dirs=None,
debug=False,
extra_preargs=None,
extra_postargs=None,
depends=None,
):
"""Compile one or more source files.
'sources' must be a list of filenames, most likely C/C++
files, but in reality anything that can be handled by a
particular compiler and compiler class (eg. MSVCCompiler can
handle resource files in 'sources'). Return a list of object
filenames, one per source filename in 'sources'. Depending on
the implementation, not all source files will necessarily be
compiled, but all corresponding object filenames will be
returned.
If 'output_dir' is given, object files will be put under it, while
retaining their original path component. That is, "foo/bar.c"
normally compiles to "foo/bar.o" (for a Unix implementation); if
'output_dir' is "build", then it would compile to
"build/foo/bar.o".
'macros', if given, must be a list of macro definitions. A macro
definition is either a (name, value) 2-tuple or a (name,) 1-tuple.
The former defines a macro; if the value is None, the macro is
defined without an explicit value. The 1-tuple case undefines a
macro. Later definitions/redefinitions/ undefinitions take
precedence.
'include_dirs', if given, must be a list of strings, the
directories to add to the default include file search path for this
compilation only.
'debug' is a boolean; if true, the compiler will be instructed to
output debug symbols in (or alongside) the object file(s).
'extra_preargs' and 'extra_postargs' are implementation- dependent.
On platforms that have the notion of a command-line (e.g. Unix,
DOS/Windows), they are most likely lists of strings: extra
command-line arguments to prepend/append to the compiler command
line. On other platforms, consult the implementation class
documentation. In any event, they are intended as an escape hatch
for those occasions when the abstract compiler framework doesn't
cut the mustard.
'depends', if given, is a list of filenames that all targets
depend on. If a source file is older than any file in
depends, then the source file will be recompiled. This
supports dependency tracking, but only at a coarse
granularity.
Raises CompileError on failure.
"""
# A concrete compiler class can either override this method
# entirely or implement _compile().
macros, objects, extra_postargs, pp_opts, build = self._setup_compile(
output_dir, macros, include_dirs, sources, depends, extra_postargs
)
cc_args = self._get_cc_args(pp_opts, debug, extra_preargs)
for obj in objects:
try:
src, ext = build[obj]
except KeyError:
continue
self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts)
# Return *all* object filenames, not just the ones we just built.
return objects
def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
"""Compile 'src' to product 'obj'."""
# A concrete compiler class that does not override compile()
# should implement _compile().
pass
def create_static_lib(
self, objects, output_libname, output_dir=None, debug=False, target_lang=None
):
"""Link a bunch of stuff together to create a static library file.
The "bunch of stuff" consists of the list of object files supplied
as 'objects', the extra object files supplied to
'add_link_object()' and/or 'set_link_objects()', the libraries
supplied to 'add_library()' and/or 'set_libraries()', and the
libraries supplied as 'libraries' (if any).
'output_libname' should be a library name, not a filename; the
filename will be inferred from the library name. 'output_dir' is
the directory where the library file will be put.
'debug' is a boolean; if true, debugging information will be
included in the library (note that on most platforms, it is the
compile step where this matters: the 'debug' flag is included here
just for consistency).
'target_lang' is the target language for which the given objects
are being compiled. This allows specific linkage time treatment of
certain languages.
Raises LibError on failure.
"""
pass
# values for target_desc parameter in link()
SHARED_OBJECT = "shared_object"
SHARED_LIBRARY = "shared_library"
EXECUTABLE = "executable"
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 a bunch of stuff together to create an executable or
shared library file.
The "bunch of stuff" consists of the list of object files supplied
as 'objects'. 'output_filename' should be a filename. If
'output_dir' is supplied, 'output_filename' is relative to it
(i.e. 'output_filename' can provide directory components if
needed).
'libraries' is a list of libraries to link against. These are
library names, not filenames, since they're translated into
filenames in a platform-specific way (eg. "foo" becomes "libfoo.a"
on Unix and "foo.lib" on DOS/Windows). However, they can include a
directory component, which means the linker will look in that
specific directory rather than searching all the normal locations.
'library_dirs', if supplied, should be a list of directories to
search for libraries that were specified as bare library names
(ie. no directory component). These are on top of the system
default and those supplied to 'add_library_dir()' and/or
'set_library_dirs()'. 'runtime_library_dirs' is a list of
directories that will be embedded into the shared library and used
to search for other shared libraries that *it* depends on at
run-time. (This may only be relevant on Unix.)
'export_symbols' is a list of symbols that the shared library will
export. (This appears to be relevant only on Windows.)
'debug' is as for 'compile()' and 'create_static_lib()', with the
slight distinction that it actually matters on most platforms (as
opposed to 'create_static_lib()', which includes a 'debug' flag
mostly for form's sake).
'extra_preargs' and 'extra_postargs' are as for 'compile()' (except
of course that they supply command-line arguments for the
particular linker being used).
'target_lang' is the target language for which the given objects
are being compiled. This allows specific linkage time treatment of
certain languages.
Raises LinkError on failure.
"""
raise NotImplementedError
# Old 'link_*()' methods, rewritten to use the new 'link()' method.
def link_shared_lib(
self,
objects,
output_libname,
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,
):
self.link(
CCompiler.SHARED_LIBRARY,
objects,
self.library_filename(output_libname, lib_type='shared'),
output_dir,
libraries,
library_dirs,
runtime_library_dirs,
export_symbols,
debug,
extra_preargs,
extra_postargs,
build_temp,
target_lang,
)
def link_shared_object(
self,
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,
):
self.link(
CCompiler.SHARED_OBJECT,
objects,
output_filename,
output_dir,
libraries,
library_dirs,
runtime_library_dirs,
export_symbols,
debug,
extra_preargs,
extra_postargs,
build_temp,
target_lang,
)
def link_executable(
self,
objects,
output_progname,
output_dir=None,
libraries=None,
library_dirs=None,
runtime_library_dirs=None,
debug=False,
extra_preargs=None,
extra_postargs=None,
target_lang=None,
):
self.link(
CCompiler.EXECUTABLE,
objects,
self.executable_filename(output_progname),
output_dir,
libraries,
library_dirs,
runtime_library_dirs,
None,
debug,
extra_preargs,
extra_postargs,
None,
target_lang,
)
# -- Miscellaneous methods -----------------------------------------
# These are all used by the 'gen_lib_options() function; there is
# no appropriate default implementation so subclasses should
# implement all of these.
def library_dir_option(self, dir):
"""Return the compiler option to add 'dir' to the list of
directories searched for libraries.
"""
raise NotImplementedError
def runtime_library_dir_option(self, dir):
"""Return the compiler option to add 'dir' to the list of
directories searched for runtime libraries.
"""
raise NotImplementedError
def library_option(self, lib):
"""Return the compiler option to add 'lib' to the list of libraries
linked into the shared library or executable.
"""
raise NotImplementedError
def has_function( # noqa: C901
self,
funcname,
includes=None,
include_dirs=None,
libraries=None,
library_dirs=None,
):
"""Return a boolean indicating whether funcname is provided as
a symbol on the current platform. The optional arguments can
be used to augment the compilation environment.
The libraries argument is a list of flags to be passed to the
linker to make additional symbol definitions available for
linking.
The includes and include_dirs arguments are deprecated.
Usually, supplying include files with function declarations
will cause function detection to fail even in cases where the
symbol is available for linking.
"""
# this can't be included at module scope because it tries to
# import math which might not be available at that point - maybe
# the necessary logic should just be inlined?
import tempfile
if includes is None:
includes = []
else:
warnings.warn("includes is deprecated", DeprecationWarning)
if include_dirs is None:
include_dirs = []
else:
warnings.warn("include_dirs is deprecated", DeprecationWarning)
if libraries is None:
libraries = []
if library_dirs is None:
library_dirs = []
fd, fname = tempfile.mkstemp(".c", funcname, text=True)
with os.fdopen(fd, "w", encoding='utf-8') as f:
for incl in includes:
f.write(f"""#include "{incl}"\n""")
if not includes:
# Use "char func(void);" as the prototype to follow
# what autoconf does. This prototype does not match
# any well-known function the compiler might recognize
# as a builtin, so this ends up as a true link test.
# Without a fake prototype, the test would need to
# know the exact argument types, and the has_function
# interface does not provide that level of information.
f.write(
f"""\
#ifdef __cplusplus
extern "C"
#endif
char {funcname}(void);
"""
)
f.write(
f"""\
int main (int argc, char **argv) {{
{funcname}();
return 0;
}}
"""
)
try:
objects = self.compile([fname], include_dirs=include_dirs)
except CompileError:
return False
finally:
os.remove(fname)
try:
self.link_executable(
objects, "a.out", libraries=libraries, library_dirs=library_dirs
)
except (LinkError, TypeError):
return False
else:
os.remove(
self.executable_filename("a.out", output_dir=self.output_dir or '')
)
finally:
for fn in objects:
os.remove(fn)
return True
def find_library_file(self, dirs, lib, debug=False):
"""Search the specified list of directories for a static or shared
library file 'lib' and return the full path to that file. If
'debug' true, look for a debugging version (if that makes sense on
the current platform). Return None if 'lib' wasn't found in any of
the specified directories.
"""
raise NotImplementedError
# -- Filename generation methods -----------------------------------
# The default implementation of the filename generating methods are
# prejudiced towards the Unix/DOS/Windows view of the world:
# * object files are named by replacing the source file extension
# (eg. .c/.cpp -> .o/.obj)
# * library files (shared or static) are named by plugging the
# library name and extension into a format string, eg.
# "lib%s.%s" % (lib_name, ".a") for Unix static libraries
# * executables are named by appending an extension (possibly
# empty) to the program name: eg. progname + ".exe" for
# Windows
#
# To reduce redundant code, these methods expect to find
# several attributes in the current object (presumably defined
# as class attributes):
# * src_extensions -
# list of C/C++ source file extensions, eg. ['.c', '.cpp']
# * obj_extension -
# object file extension, eg. '.o' or '.obj'
# * static_lib_extension -
# extension for static library files, eg. '.a' or '.lib'
# * shared_lib_extension -
# extension for shared library/object files, eg. '.so', '.dll'
# * static_lib_format -
# format string for generating static library filenames,
# eg. 'lib%s.%s' or '%s.%s'
# * shared_lib_format
# format string for generating shared library filenames
# (probably same as static_lib_format, since the extension
# is one of the intended parameters to the format string)
# * exe_extension -
# extension for executable files, eg. '' or '.exe'
def object_filenames(self, source_filenames, strip_dir=False, output_dir=''):
if output_dir is None:
output_dir = ''
return list(
self._make_out_path(output_dir, strip_dir, src_name)
for src_name in source_filenames
)
@property
def out_extensions(self):
return dict.fromkeys(self.src_extensions, self.obj_extension)
def _make_out_path(self, output_dir, strip_dir, src_name):
base, ext = os.path.splitext(src_name)
base = self._make_relative(base)
try:
new_ext = self.out_extensions[ext]
except LookupError:
raise UnknownFileError(f"unknown file type '{ext}' (from '{src_name}')")
if strip_dir:
base = os.path.basename(base)
return os.path.join(output_dir, base + new_ext)
@staticmethod
def _make_relative(base):
"""
In order to ensure that a filename always honors the
indicated output_dir, make sure it's relative.
Ref python/cpython#37775.
"""
# Chop off the drive
no_drive = os.path.splitdrive(base)[1]
# If abs, chop off leading /
return no_drive[os.path.isabs(no_drive) :]
def shared_object_filename(self, basename, strip_dir=False, output_dir=''):
assert output_dir is not None
if strip_dir:
basename = os.path.basename(basename)
return os.path.join(output_dir, basename + self.shared_lib_extension)
def executable_filename(self, basename, strip_dir=False, output_dir=''):
assert output_dir is not None
if strip_dir:
basename = os.path.basename(basename)
return os.path.join(output_dir, basename + (self.exe_extension or ''))
def library_filename(
self,
libname,
lib_type='static',
strip_dir=False,
output_dir='', # or 'shared'
):
assert output_dir is not None
expected = '"static", "shared", "dylib", "xcode_stub"'
if lib_type not in eval(expected):
raise ValueError(f"'lib_type' must be {expected}")
fmt = getattr(self, lib_type + "_lib_format")
ext = getattr(self, lib_type + "_lib_extension")
dir, base = os.path.split(libname)
filename = fmt % (base, ext)
if strip_dir:
dir = ''
return os.path.join(output_dir, dir, filename)
# -- Utility methods -----------------------------------------------
def announce(self, msg, level=1):
log.debug(msg)
def debug_print(self, msg):
from distutils.debug import DEBUG
if DEBUG:
print(msg)
def warn(self, msg):
sys.stderr.write(f"warning: {msg}\n")
def execute(self, func, args, msg=None, level=1):
execute(func, args, msg, self.dry_run)
def spawn(self, cmd, **kwargs):
spawn(cmd, dry_run=self.dry_run, **kwargs)
def move_file(self, src, dst):
return move_file(src, dst, dry_run=self.dry_run)
def mkpath(self, name, mode=0o777):
mkpath(name, mode, dry_run=self.dry_run)
# Map a sys.platform/os.name ('posix', 'nt') to the default compiler
# type for that platform. Keys are interpreted as re match
# patterns. Order is important; platform mappings are preferred over
# OS names.
_default_compilers = (
# Platform string mappings
# on a cygwin built python we can use gcc like an ordinary UNIXish
# compiler
('cygwin.*', 'unix'),
('zos', 'zos'),
# OS name mappings
('posix', 'unix'),
('nt', 'msvc'),
)
from .compilers.C.errors import CompileError, LinkError
__all__ = [
'CompileError',
'LinkError',
'gen_lib_options',
'gen_preprocess_options',
'get_default_compiler',
'new_compiler',
'show_compilers',
]
def get_default_compiler(osname=None, platform=None):
"""Determine the default compiler to use for the given platform.
CCompiler = base.Compiler
osname should be one of the standard Python OS names (i.e. the
ones returned by os.name) and platform the common value
returned by sys.platform for the platform in question.
The default values are os.name and sys.platform in case the
parameters are not given.
"""
if osname is None:
osname = os.name
if platform is None:
platform = sys.platform
# Mingw is a special case where sys.platform is 'win32' but we
# want to use the 'mingw32' compiler, so check it first
if is_mingw():
return 'mingw32'
for pattern, compiler in _default_compilers:
if (
re.match(pattern, platform) is not None
or re.match(pattern, osname) is not None
):
return compiler
# Default to Unix compiler
return 'unix'
# Map compiler types to (module_name, class_name) pairs -- ie. where to
# find the code that implements an interface to this compiler. (The module
# is assumed to be in the 'distutils' package.)
compiler_class = {
'unix': ('unixccompiler', 'UnixCCompiler', "standard UNIX-style compiler"),
'msvc': ('_msvccompiler', 'MSVCCompiler', "Microsoft Visual C++"),
'cygwin': (
'cygwinccompiler',
'CygwinCCompiler',
"Cygwin port of GNU C Compiler for Win32",
),
'mingw32': (
'cygwinccompiler',
'Mingw32CCompiler',
"Mingw32 port of GNU C Compiler for Win32",
),
'bcpp': ('bcppcompiler', 'BCPPCompiler', "Borland C++ Compiler"),
'zos': ('zosccompiler', 'zOSCCompiler', 'IBM XL C/C++ Compilers'),
}
def show_compilers():
"""Print list of available compilers (used by the "--help-compiler"
options to "build", "build_ext", "build_clib").
"""
# XXX this "knows" that the compiler option it's describing is
# "--compiler", which just happens to be the case for the three
# commands that use it.
from distutils.fancy_getopt import FancyGetopt
compilers = sorted(
("compiler=" + compiler, None, compiler_class[compiler][2])
for compiler in compiler_class.keys()
)
pretty_printer = FancyGetopt(compilers)
pretty_printer.print_help("List of available compilers:")
def new_compiler(plat=None, compiler=None, verbose=False, dry_run=False, force=False):
"""Generate an instance of some CCompiler subclass for the supplied
platform/compiler combination. 'plat' defaults to 'os.name'
(eg. 'posix', 'nt'), and 'compiler' defaults to the default compiler
for that platform. Currently only 'posix' and 'nt' are supported, and
the default compilers are "traditional Unix interface" (UnixCCompiler
class) and Visual C++ (MSVCCompiler class). Note that it's perfectly
possible to ask for a Unix compiler object under Windows, and a
Microsoft compiler object under Unix -- if you supply a value for
'compiler', 'plat' is ignored.
"""
if plat is None:
plat = os.name
try:
if compiler is None:
compiler = get_default_compiler(plat)
(module_name, class_name, long_description) = compiler_class[compiler]
except KeyError:
msg = f"don't know how to compile C/C++ code on platform '{plat}'"
if compiler is not None:
msg = msg + f" with '{compiler}' compiler"
raise DistutilsPlatformError(msg)
try:
module_name = "distutils." + module_name
__import__(module_name)
module = sys.modules[module_name]
klass = vars(module)[class_name]
except ImportError:
raise DistutilsModuleError(
f"can't compile C/C++ code: unable to load module '{module_name}'"
)
except KeyError:
raise DistutilsModuleError(
f"can't compile C/C++ code: unable to find class '{class_name}' "
f"in module '{module_name}'"
)
# XXX The None is necessary to preserve backwards compatibility
# with classes that expect verbose to be the first positional
# argument.
return klass(None, dry_run, force)
def gen_preprocess_options(macros, include_dirs):
"""Generate C pre-processor options (-D, -U, -I) as used by at least
two types of compilers: the typical Unix compiler and Visual C++.
'macros' is the usual thing, a list of 1- or 2-tuples, where (name,)
means undefine (-U) macro 'name', and (name,value) means define (-D)
macro 'name' to 'value'. 'include_dirs' is just a list of directory
names to be added to the header file search path (-I). Returns a list
of command-line options suitable for either Unix compilers or Visual
C++.
"""
# XXX it would be nice (mainly aesthetic, and so we don't generate
# stupid-looking command lines) to go over 'macros' and eliminate
# redundant definitions/undefinitions (ie. ensure that only the
# latest mention of a particular macro winds up on the command
# line). I don't think it's essential, though, since most (all?)
# Unix C compilers only pay attention to the latest -D or -U
# mention of a macro on their command line. Similar situation for
# 'include_dirs'. I'm punting on both for now. Anyways, weeding out
# redundancies like this should probably be the province of
# CCompiler, since the data structures used are inherited from it
# and therefore common to all CCompiler classes.
pp_opts = []
for macro in macros:
if not (isinstance(macro, tuple) and 1 <= len(macro) <= 2):
raise TypeError(
f"bad macro definition '{macro}': "
"each element of 'macros' list must be a 1- or 2-tuple"
)
if len(macro) == 1: # undefine this macro
pp_opts.append(f"-U{macro[0]}")
elif len(macro) == 2:
if macro[1] is None: # define with no explicit value
pp_opts.append(f"-D{macro[0]}")
else:
# XXX *don't* need to be clever about quoting the
# macro value here, because we're going to avoid the
# shell at all costs when we spawn the command!
pp_opts.append("-D{}={}".format(*macro))
pp_opts.extend(f"-I{dir}" for dir in include_dirs)
return pp_opts
def gen_lib_options(compiler, library_dirs, runtime_library_dirs, libraries):
"""Generate linker options for searching library directories and
linking with specific libraries. 'libraries' and 'library_dirs' are,
respectively, lists of library names (not filenames!) and search
directories. Returns a list of command-line options suitable for use
with some compiler (depending on the two format strings passed in).
"""
lib_opts = [compiler.library_dir_option(dir) for dir in library_dirs]
for dir in runtime_library_dirs:
lib_opts.extend(always_iterable(compiler.runtime_library_dir_option(dir)))
# XXX it's important that we *not* remove redundant library mentions!
# sometimes you really do have to say "-lfoo -lbar -lfoo" in order to
# resolve all symbols. I just hope we never have to say "-lfoo obj.o
# -lbar" to get things to work -- that's certainly a possibility, but a
# pretty nasty way to arrange your C code.
for lib in libraries:
(lib_dir, lib_name) = os.path.split(lib)
if lib_dir:
lib_file = compiler.find_library_file([lib_dir], lib_name)
if lib_file:
lib_opts.append(lib_file)
else:
compiler.warn(
f"no library file corresponding to '{lib}' found (skipping)"
)
else:
lib_opts.append(compiler.library_option(lib))
return lib_opts

@@ -7,4 +7,2 @@ """distutils.cmd

from __future__ import annotations
import logging

@@ -14,5 +12,2 @@ import os

import sys
from abc import abstractmethod
from collections.abc import Callable, MutableSequence
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, overload

@@ -23,15 +18,3 @@ from . import _modified, archive_util, dir_util, file_util, util

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:

@@ -67,14 +50,7 @@ """Abstract base class for defining command classes, the "worker bees"

# defined. The canonical example is the "install" command.
sub_commands: ClassVar[ # Any to work around variance issues
list[tuple[str, Callable[[Any], bool] | None]]
] = []
sub_commands = []
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:
def __init__(self, dist):
"""Create and initialize a new Command object. Most importantly,

@@ -137,3 +113,3 @@ invokes the 'initialize_options()' method, which is the real

def ensure_finalized(self) -> None:
def ensure_finalized(self):
if not self.finalized:

@@ -156,4 +132,3 @@ self.finalize_options()

@abstractmethod
def initialize_options(self) -> None:
def initialize_options(self):
"""Set default values for all the options that this command

@@ -172,4 +147,3 @@ supports. Note that these defaults may be overridden by other

@abstractmethod
def finalize_options(self) -> None:
def finalize_options(self):
"""Set final values for all the options that this command supports.

@@ -203,4 +177,3 @@ This is always called as late as possible, ie. after any option

@abstractmethod
def run(self) -> None:
def run(self):
"""A command's raison d'etre: carry out the action it exists to

@@ -219,6 +192,6 @@ perform, controlled by the options initialized in

def announce(self, msg: object, level: int = logging.DEBUG) -> None:
def announce(self, msg, level=logging.DEBUG):
log.log(level, msg)
def debug_print(self, msg: object) -> None:
def debug_print(self, msg):
"""Print 'msg' to stdout if the global DEBUG (taken from the

@@ -255,3 +228,3 @@ DISTUTILS_DEBUG environment variable) flag is true.

def ensure_string(self, option: str, default: str | None = None) -> None:
def ensure_string(self, option, default=None):
"""Ensure that 'option' is a string; if not defined, set it to

@@ -262,3 +235,3 @@ 'default'.

def ensure_string_list(self, option: str) -> None:
def ensure_string_list(self, option):
r"""Ensure that 'option' is a list of strings. If 'option' is

@@ -291,3 +264,3 @@ currently a string, we split it either on /,\s*/ or /\s+/, so

def ensure_filename(self, option: str) -> None:
def ensure_filename(self, option):
"""Ensure that 'option' is the name of an existing file."""

@@ -298,3 +271,3 @@ self._ensure_tested_string(

def ensure_dirname(self, option: str) -> None:
def ensure_dirname(self, option):
self._ensure_tested_string(

@@ -309,3 +282,3 @@ option,

def get_command_name(self) -> str:
def get_command_name(self):
if hasattr(self, 'command_name'):

@@ -316,5 +289,3 @@ return self.command_name

def set_undefined_options(
self, src_cmd: str, *option_pairs: tuple[str, str]
) -> None:
def set_undefined_options(self, src_cmd, *option_pairs):
"""Set the values of any "undefined" options from corresponding

@@ -340,5 +311,3 @@ option values in some other command object. "Undefined" here means

# 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:
def get_finalized_command(self, command, create=True):
"""Wrapper around Distribution's 'get_command_obj()' method: find

@@ -355,16 +324,6 @@ (create if necessary and 'create' is true) the command object for

# 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:
def reinitialize_command(self, command, reinit_subcommands=False):
return self.distribution.reinitialize_command(command, reinit_subcommands)
def run_command(self, command: str) -> None:
def run_command(self, command):
"""Run some other command: uses the 'run_command()' method of

@@ -376,3 +335,3 @@ Distribution, which creates and finalizes the command object if

def get_sub_commands(self) -> list[str]:
def get_sub_commands(self):
"""Determine the sub-commands that are relevant in the current

@@ -392,46 +351,20 @@ distribution (ie., that need to be run). This is based on the

def warn(self, msg: object) -> None:
def warn(self, msg):
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:
def execute(self, func, args, msg=None, level=1):
util.execute(func, args, msg, dry_run=self.dry_run)
def mkpath(self, name: str, mode: int = 0o777) -> None:
def mkpath(self, name, mode=0o777):
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]:
infile,
outfile,
preserve_mode=True,
preserve_times=True,
link=None,
level=1,
):
"""Copy a file respecting verbose, dry-run and force flags. (The

@@ -452,9 +385,9 @@ former two default to whatever is in the Distribution object, and

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]:
infile,
outfile,
preserve_mode=True,
preserve_times=True,
preserve_symlinks=False,
level=1,
):
"""Copy an entire directory tree respecting verbose, dry-run,

@@ -473,22 +406,7 @@ and force flags.

@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]:
def move_file(self, src, dst, level=1):
"""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:
def spawn(self, cmd, search_path=True, level=1):
"""Spawn an external command respecting dry-run flag."""

@@ -499,31 +417,5 @@ from distutils.spawn import spawn

@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:
self, base_name, format, root_dir=None, base_dir=None, owner=None, group=None
):
return archive_util.make_archive(

@@ -540,11 +432,4 @@ base_name,

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:
self, infiles, outfile, func, args, exec_msg=None, skip_msg=None, level=1
):
"""Special case of 'execute()' for operations that process one or

@@ -551,0 +436,0 @@ more input files and generate one output file. Works just like

@@ -9,3 +9,2 @@ """distutils.command.bdist_dumb

from distutils._log import log
from typing import ClassVar

@@ -59,3 +58,3 @@ from ..core import Command

boolean_options: ClassVar[list[str]] = ['keep-temp', 'skip-build', 'relative']
boolean_options = ['keep-temp', 'skip-build', 'relative']

@@ -62,0 +61,0 @@ default_format = {'posix': 'gztar', 'nt': 'zip'}

@@ -10,3 +10,2 @@ """distutils.command.bdist_rpm

from distutils._log import log
from typing import ClassVar

@@ -141,3 +140,3 @@ from ..core import Command

boolean_options: ClassVar[list[str]] = [
boolean_options = [
'keep-temp',

@@ -150,3 +149,3 @@ 'use-rpm-opt-flags',

negative_opt: ClassVar[dict[str, str]] = {
negative_opt = {
'no-keep-temp': 'keep-temp',

@@ -202,3 +201,3 @@ 'no-rpm-opt-flags': 'use-rpm-opt-flags',

def finalize_options(self) -> None:
def finalize_options(self):
self.set_undefined_options('bdist', ('bdist_base', 'bdist_base'))

@@ -236,3 +235,3 @@ if self.rpm_base is None:

def finalize_package_data(self) -> None:
def finalize_package_data(self):
self.ensure_string('group', "Development/Libraries")

@@ -283,3 +282,3 @@ self.ensure_string(

def run(self) -> None: # noqa: C901
def run(self): # noqa: C901
if DEBUG:

@@ -286,0 +285,0 @@ print("before _get_package_data():")

@@ -6,8 +6,4 @@ """distutils.command.bdist

from __future__ import annotations
import os
import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING, ClassVar

@@ -18,10 +14,3 @@ from ..core import Command

if TYPE_CHECKING:
from typing_extensions import deprecated
else:
def deprecated(message):
return lambda fn: fn
def show_formats():

@@ -39,8 +28,7 @@ """Print list of available formats (arguments to "--format" option)."""

class ListCompat(dict[str, tuple[str, str]]):
class ListCompat(dict):
# 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:
def append(self, item):
warnings.warn(
"format_commands is now a dict. append is deprecated.",
"""format_commands is now a dict. append is deprecated.""",
DeprecationWarning,

@@ -81,5 +69,5 @@ stacklevel=2,

boolean_options: ClassVar[list[str]] = ['skip-build']
boolean_options = ['skip-build']
help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
help_options = [
('help-formats', None, "lists available distribution formats", show_formats),

@@ -89,7 +77,7 @@ ]

# The following commands do not take a format option from bdist
no_format_option: ClassVar[tuple[str, ...]] = ('bdist_rpm',)
no_format_option = ('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'}
default_format = {'posix': 'gztar', 'nt': 'zip'}

@@ -119,3 +107,3 @@ # Define commands in preferred order for the --help-formats option

def finalize_options(self) -> None:
def finalize_options(self):
# have to finalize 'plat_name' before 'bdist_base'

@@ -148,3 +136,3 @@ if self.plat_name is None:

def run(self) -> None:
def run(self):
# Figure out which sub-commands we need to run.

@@ -151,0 +139,0 @@ commands = []

@@ -7,2 +7,3 @@ """distutils.command.build_clib

# XXX this module has *lots* of code ripped-off quite transparently from

@@ -16,10 +17,6 @@ # build_ext.py -- not surprisingly really, as the work required to build

# 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

@@ -30,6 +27,12 @@ from ..errors import DistutilsSetupError

def show_compilers():
from ..ccompiler import show_compilers
show_compilers()
class build_clib(Command):
description = "build C/C++ libraries used by Python extensions"
user_options: ClassVar[list[tuple[str, str, str]]] = [
user_options = [
('build-clib=', 'b', "directory to build C/C++ libraries to"),

@@ -42,5 +45,5 @@ ('build-temp=', 't', "directory to put temporary build by-products"),

boolean_options: ClassVar[list[str]] = ['debug', 'force']
boolean_options = ['debug', 'force']
help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
help_options = [
('help-compiler', None, "list available compilers", show_compilers),

@@ -64,3 +67,3 @@ ]

def finalize_options(self) -> None:
def finalize_options(self):
# This might be confusing: both build-clib and build-temp default

@@ -92,6 +95,9 @@ # to build-temp as defined by the "build" command. This is because

def run(self) -> None:
def run(self):
if not self.libraries:
return
# Yech -- this is cut 'n pasted from build_ext.py!
from ..ccompiler import new_compiler
self.compiler = new_compiler(

@@ -114,3 +120,3 @@ compiler=self.compiler, dry_run=self.dry_run, force=self.force

def check_library_list(self, libraries) -> None:
def check_library_list(self, libraries):
"""Ensure that the list of libraries is valid.

@@ -142,3 +148,4 @@

raise DistutilsSetupError(
f"bad library name '{lib[0]}': may not contain directory separators"
f"bad library name '{lib[0]}': "
"may not contain directory separators"
)

@@ -178,3 +185,3 @@

def build_libraries(self, libraries) -> None:
def build_libraries(self, libraries):
for lib_name, build_info in libraries:

@@ -181,0 +188,0 @@ sources = build_info.get('sources')

@@ -7,4 +7,2 @@ """distutils.command.build_ext

from __future__ import annotations
import contextlib

@@ -14,9 +12,6 @@ import os

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

@@ -33,3 +28,3 @@ from ..errors import (

from ..sysconfig import customize_compiler, get_config_h_filename, get_python_version
from ..util import get_platform, is_freethreaded, is_mingw
from ..util import get_platform, is_mingw

@@ -41,2 +36,8 @@ # An extension name is just a dot-separated list of Python NAMEs (ie.

def show_compilers():
from ..ccompiler import show_compilers
show_compilers()
class build_ext(Command):

@@ -104,11 +105,5 @@ description = "build C/C++ extensions (compile/link to build directory)"

boolean_options: ClassVar[list[str]] = [
'inplace',
'debug',
'force',
'swig-cpp',
'user',
]
boolean_options = ['inplace', 'debug', 'force', 'swig-cpp', 'user']
help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
help_options = [
('help-compiler', None, "list available compilers", show_compilers),

@@ -166,3 +161,3 @@ ]

def finalize_options(self) -> None: # noqa: C901
def finalize_options(self): # noqa: C901
from distutils import sysconfig

@@ -306,3 +301,5 @@

def run(self) -> None: # noqa: C901
def run(self): # noqa: C901
from ..ccompiler import new_compiler
# 'self.extensions', as supplied by setup.py, is a list of

@@ -346,8 +343,2 @@ # Extension instances. See the documentation for Extension (in

# 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

@@ -378,3 +369,3 @@ # come from the command-line or from the setup script) are set in

def check_extensions_list(self, extensions) -> None: # noqa: C901
def check_extensions_list(self, extensions): # noqa: C901
"""Ensure that the list of extensions (presumably provided as a

@@ -458,3 +449,4 @@ command option 'extensions') is valid, i.e. it is a list of

raise DistutilsSetupError(
"'macros' element of build info dict must be 1- or 2-tuple"
"'macros' element of build info dict "
"must be 1- or 2-tuple"
)

@@ -488,3 +480,3 @@ if len(macro) == 1:

def build_extensions(self) -> None:
def build_extensions(self):
# First, sanity-check the 'extensions' list

@@ -532,3 +524,3 @@ self.check_extensions_list(self.extensions)

def build_extension(self, ext) -> None:
def build_extension(self, ext):
sources = ext.sources

@@ -689,3 +681,4 @@ if sources is None or not isinstance(sources, (list, tuple)):

raise DistutilsPlatformError(
f"I don't know how to find (much less run) SWIG on platform '{os.name}'"
"I don't know how to find (much less run) SWIG "
f"on platform '{os.name}'"
)

@@ -695,3 +688,3 @@

# (extension names, filenames, whatever)
def get_ext_fullpath(self, ext_name: str) -> str:
def get_ext_fullpath(self, ext_name):
"""Returns the path of the filename for a given extension.

@@ -723,3 +716,3 @@

def get_ext_fullname(self, ext_name: str) -> str:
def get_ext_fullname(self, ext_name):
"""Returns the fullname of a given extension name.

@@ -733,3 +726,3 @@

def get_ext_filename(self, ext_name: str) -> str:
def get_ext_filename(self, ext_name):
r"""Convert the name of an extension (eg. "foo.bar") into the name

@@ -745,3 +738,3 @@ of the file from which it will be loaded (eg. "foo/bar.so", or

def get_export_symbols(self, ext: Extension) -> list[str]:
def get_export_symbols(self, ext):
"""Return the list of symbols that a shared extension has to

@@ -752,3 +745,3 @@ export. This either uses 'ext.export_symbols' or, if it's not

"""
name = self._get_module_name_for_symbol(ext)
name = ext.name.split('.')[-1]
try:

@@ -768,12 +761,3 @@ # Unicode module name support as defined in PEP-489

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
def get_libraries(self, ext): # noqa: C901
"""Return the list of libraries to link against when building a

@@ -780,0 +764,0 @@ shared extension. On most platforms, this is just 'ext.libraries';

@@ -10,3 +10,2 @@ """distutils.command.build_py

from distutils._log import log
from typing import ClassVar

@@ -34,4 +33,4 @@ from ..core import Command

boolean_options: ClassVar[list[str]] = ['compile', 'force']
negative_opt: ClassVar[dict[str, str]] = {'no-compile': 'compile'}
boolean_options = ['compile', 'force']
negative_opt = {'no-compile': 'compile'}

@@ -48,3 +47,3 @@ def initialize_options(self):

def finalize_options(self) -> None:
def finalize_options(self):
self.set_undefined_options(

@@ -74,3 +73,3 @@ 'build', ('build_lib', 'build_lib'), ('force', 'force')

def run(self) -> None:
def run(self):
# XXX copy_file by default preserves atime and mtime. IMHO this is

@@ -142,3 +141,3 @@ # the right thing to do, but perhaps it should be an option -- in

def build_package_data(self) -> None:
def build_package_data(self):
"""Copy data files into build directory"""

@@ -315,3 +314,3 @@ for _package, src_dir, build_dir, filenames in self.data_files:

def get_outputs(self, include_bytecode: bool = True) -> list[str]:
def get_outputs(self, include_bytecode=True):
modules = self.find_all_modules()

@@ -359,3 +358,3 @@ outputs = []

def build_modules(self) -> None:
def build_modules(self):
modules = self.find_modules()

@@ -369,3 +368,3 @@ for package, module, module_file in modules:

def build_packages(self) -> None:
def build_packages(self):
for package in self.packages:

@@ -390,3 +389,3 @@ # Get list of (package, module, module_file) tuples based on

def byte_compile(self, files) -> None:
def byte_compile(self, files):
if sys.dont_write_bytecode:

@@ -393,0 +392,0 @@ self.warn('byte-compiling is disabled, skipping.')

@@ -8,5 +8,5 @@ """distutils.command.build_scripts

import tokenize
from distutils import sysconfig
from distutils._log import log
from stat import ST_MODE
from typing import ClassVar

@@ -29,3 +29,3 @@ from .._modified import newer

user_options: ClassVar[list[tuple[str, str, str]]] = [
user_options = [
('build-dir=', 'd', "directory to \"build\" (copy) to"),

@@ -36,3 +36,3 @@ ('force', 'f', "forcibly build everything (ignore file timestamps"),

boolean_options: ClassVar[list[str]] = ['force']
boolean_options = ['force']

@@ -81,3 +81,3 @@ def initialize_options(self):

def _copy_script(self, script, outfiles, updated_files):
def _copy_script(self, script, outfiles, updated_files): # noqa: C901
shebang_match = None

@@ -112,4 +112,14 @@ script = convert_path(script)

if not self.dry_run:
if not sysconfig.python_build:
executable = self.executable
else:
executable = os.path.join(
sysconfig.get_config_var("BINDIR"),
"python{}{}".format(
sysconfig.get_config_var("VERSION"),
sysconfig.get_config_var("EXE"),
),
)
post_interp = shebang_match.group(1) or ''
shebang = "#!" + self.executable + post_interp + "\n"
shebang = "#!" + executable + post_interp + "\n"
self._validate_shebang(shebang, f.encoding)

@@ -116,0 +126,0 @@ with open(outfile, "w", encoding=f.encoding) as outf:

@@ -5,11 +5,6 @@ """distutils.command.build

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

@@ -20,2 +15,8 @@ from ..errors import DistutilsOptionError

def show_compilers():
from ..ccompiler import show_compilers
show_compilers()
class build(Command):

@@ -47,5 +48,5 @@ description = "build everything needed to install"

boolean_options: ClassVar[list[str]] = ['debug', 'force']
boolean_options = ['debug', 'force']
help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
help_options = [
('help-compiler', None, "list available compilers", show_compilers),

@@ -70,3 +71,3 @@ ]

def finalize_options(self) -> None: # noqa: C901
def finalize_options(self): # noqa: C901
if self.plat_name is None:

@@ -119,4 +120,3 @@ self.plat_name = get_platform()

self.build_scripts = os.path.join(
self.build_base,
f'scripts-{sys.version_info.major}.{sys.version_info.minor}',
self.build_base, 'scripts-%d.%d' % sys.version_info[:2]
)

@@ -133,3 +133,3 @@

def run(self) -> None:
def run(self):
# Run all relevant sub-commands. This will be some subset of:

@@ -136,0 +136,0 @@ # - build_py - pure Python modules

@@ -7,3 +7,2 @@ """distutils.command.check

import contextlib
from typing import ClassVar

@@ -46,3 +45,3 @@ from ..core import Command

description = "perform some checks on the package"
user_options: ClassVar[list[tuple[str, str, str]]] = [
user_options = [
('metadata', 'm', 'Verify meta-data'),

@@ -52,3 +51,6 @@ (

'r',
'Checks if long string meta-data syntax are reStructuredText-compliant',
(
'Checks if long string meta-data syntax '
'are reStructuredText-compliant'
),
),

@@ -58,3 +60,3 @@ ('strict', 's', 'Will exit with an error if a check fails'),

boolean_options: ClassVar[list[str]] = ['metadata', 'restructuredtext', 'strict']
boolean_options = ['metadata', 'restructuredtext', 'strict']

@@ -148,3 +150,3 @@ def initialize_options(self):

parser.parse(data, document)
except (AttributeError, TypeError) as e:
except AttributeError as e:
reporter.messages.append((

@@ -151,0 +153,0 @@ -1,

@@ -9,3 +9,2 @@ """distutils.command.clean

from distutils._log import log
from typing import ClassVar

@@ -35,3 +34,3 @@ from ..core import Command

boolean_options: ClassVar[list[str]] = ['all']
boolean_options = ['all']

@@ -38,0 +37,0 @@ def initialize_options(self):

@@ -20,3 +20,2 @@ """distutils.command.config

from ..ccompiler import CCompiler, CompileError, LinkError, new_compiler
from ..core import Command

@@ -93,2 +92,6 @@ from ..errors import DistutilsExecError

"""
# We do this late, and only on-demand, because this is an expensive
# import.
from ..ccompiler import CCompiler, new_compiler
if not isinstance(self.compiler, CCompiler):

@@ -179,2 +182,4 @@ self.compiler = new_compiler(

"""
from ..ccompiler import CompileError
self._check_compiler()

@@ -214,2 +219,4 @@ ok = True

"""
from ..ccompiler import CompileError
self._check_compiler()

@@ -239,2 +246,4 @@ try:

"""
from ..ccompiler import CompileError, LinkError
self._check_compiler()

@@ -264,2 +273,4 @@ try:

"""
from ..ccompiler import CompileError, LinkError
self._check_compiler()

@@ -266,0 +277,0 @@ try:

@@ -12,4 +12,3 @@ """distutils.command.install_data

import os
from collections.abc import Iterable
from typing import ClassVar
from typing import Iterable

@@ -27,3 +26,4 @@ from ..core import Command

'd',
"base directory for installing data files [default: installation base dir]",
"base directory for installing data files "
"[default: installation base dir]",
),

@@ -34,3 +34,3 @@ ('root=', None, "install everything relative to this alternate root directory"),

boolean_options: ClassVar[list[str]] = ['force']
boolean_options = ['force']

@@ -45,3 +45,3 @@ def initialize_options(self):

def finalize_options(self) -> None:
def finalize_options(self):
self.set_undefined_options(

@@ -54,3 +54,3 @@ 'install',

def run(self) -> None:
def run(self):
self.mkpath(self.install_dir)

@@ -57,0 +57,0 @@ for f in self.data_files:

@@ -11,3 +11,2 @@ """

import sys
from typing import ClassVar

@@ -23,3 +22,3 @@ from .. import dir_util

description = "Install package's PKG-INFO metadata as an .egg-info file"
user_options: ClassVar[list[tuple[str, str, str]]] = [
user_options = [
('install-dir=', 'd', "directory to install to"),

@@ -37,5 +36,7 @@ ]

"""
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"
return "%s-%s-py%d.%d.egg-info" % (
to_filename(safe_name(self.distribution.get_name())),
to_filename(safe_version(self.distribution.get_version())),
*sys.version_info[:2],
)

@@ -42,0 +43,0 @@ def finalize_options(self):

@@ -6,4 +6,2 @@ """distutils.command.install_headers

from typing import ClassVar
from ..core import Command

@@ -16,3 +14,3 @@

user_options: ClassVar[list[tuple[str, str, str]]] = [
user_options = [
('install-dir=', 'd', "directory to install header files to"),

@@ -22,3 +20,3 @@ ('force', 'f', "force installation (overwrite existing files)"),

boolean_options: ClassVar[list[str]] = ['force']
boolean_options = ['force']

@@ -25,0 +23,0 @@ def initialize_options(self):

@@ -6,8 +6,5 @@ """distutils.command.install_lib

from __future__ import annotations
import importlib.util
import os
import sys
from typing import Any, ClassVar

@@ -54,4 +51,4 @@ from ..core import Command

boolean_options: ClassVar[list[str]] = ['force', 'compile', 'skip-build']
negative_opt: ClassVar[dict[str, str]] = {'no-compile': 'compile'}
boolean_options = ['force', 'compile', 'skip-build']
negative_opt = {'no-compile': 'compile'}

@@ -67,3 +64,3 @@ def initialize_options(self):

def finalize_options(self) -> None:
def finalize_options(self):
# Get all the information we need to install pure Python modules

@@ -95,3 +92,3 @@ # from the umbrella 'install' command -- build (source) directory,

def run(self) -> None:
def run(self):
# Make sure we have built everything we need first

@@ -112,3 +109,3 @@ self.build()

def build(self) -> None:
def build(self):
if not self.skip_build:

@@ -120,4 +117,3 @@ if self.distribution.has_pure_modules():

# Any: https://typing.readthedocs.io/en/latest/guides/writing_stubs.html#the-any-trick
def install(self) -> list[str] | Any:
def install(self):
if os.path.isdir(self.build_dir):

@@ -132,3 +128,3 @@ outfiles = self.copy_tree(self.build_dir, self.install_dir)

def byte_compile(self, files) -> None:
def byte_compile(self, files):
if sys.dont_write_bytecode:

@@ -135,0 +131,0 @@ self.warn('byte-compiling is disabled, skipping.')

@@ -11,3 +11,2 @@ """distutils.command.install_scripts

from stat import ST_MODE
from typing import ClassVar

@@ -27,3 +26,3 @@ from ..core import Command

boolean_options: ClassVar[list[str]] = ['force', 'skip-build']
boolean_options = ['force', 'skip-build']

@@ -36,3 +35,3 @@ def initialize_options(self):

def finalize_options(self) -> None:
def finalize_options(self):
self.set_undefined_options('build', ('build_scripts', 'build_dir'))

@@ -46,3 +45,3 @@ self.set_undefined_options(

def run(self) -> None:
def run(self):
if not self.skip_build:

@@ -49,0 +48,0 @@ self.run_command('build_scripts')

@@ -5,5 +5,2 @@ """distutils.command.install

from __future__ import annotations
import collections
import contextlib

@@ -16,4 +13,5 @@ import itertools

from site import USER_BASE, USER_SITE
from typing import ClassVar
import jaraco.collections
from ..core import Command

@@ -149,3 +147,3 @@ from ..debug import DEBUG

except Exception:
resolved = fw.scheme(name)
resolved = fw.scheme(_pypy_hack(name))
return resolved

@@ -167,3 +165,3 @@

# have defined headers.
fallback = _load_scheme(name)
fallback = _load_scheme(_pypy_hack(name))
scheme.setdefault('headers', fallback['headers'])

@@ -178,2 +176,10 @@ return scheme

def _pypy_hack(name):
PY37 = sys.version_info < (3, 8)
old_pypy = hasattr(sys, 'pypy_version_info') and PY37
prefix = not name.endswith(('_user', '_home'))
pypy_name = 'pypy' + '_nt' * (os.name == 'nt')
return pypy_name if old_pypy and prefix else name
class install(Command):

@@ -240,3 +246,3 @@ description = "install everything from build directory"

boolean_options: ClassVar[list[str]] = ['compile', 'force', 'skip-build']
boolean_options = ['compile', 'force', 'skip-build']

@@ -251,11 +257,11 @@ if HAS_USER_SITE:

negative_opt: ClassVar[dict[str, str]] = {'no-compile': 'compile'}
negative_opt = {'no-compile': 'compile'}
def initialize_options(self) -> None:
def initialize_options(self):
"""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.prefix = None
self.exec_prefix = None
self.home = None
self.user = False

@@ -268,3 +274,3 @@

self.install_platbase = None
self.root: str | None = None
self.root = None

@@ -278,3 +284,3 @@ # These options are the actual installation directories; if not

self.install_headers = None # for C/C++ headers
self.install_lib: str | None = None # set to either purelib or platlib
self.install_lib = None # set to either purelib or platlib
self.install_scripts = None

@@ -333,3 +339,3 @@ self.install_data = None

def finalize_options(self) -> None: # noqa: C901
def finalize_options(self): # noqa: C901
"""Finalizes options."""

@@ -414,4 +420,4 @@ # This method (and its helpers, like 'finalize_unix()',

'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}',
'py_version_short': '%d.%d' % sys.version_info[:2],
'py_version_nodot': '%d%d' % sys.version_info[:2],
'sys_prefix': prefix,

@@ -437,8 +443,8 @@ 'prefix': prefix,

self.config_vars = collections.ChainMap(
self.config_vars = jaraco.collections.DictStack([
fw.vars(),
compat_vars,
sysconfig.get_config_vars(),
local_vars,
sysconfig.get_config_vars(),
compat_vars,
fw.vars(),
)
])

@@ -519,3 +525,3 @@ self.expand_basedirs()

def dump_dirs(self, msg) -> None:
def dump_dirs(self, msg):
"""Dumps the list of user options."""

@@ -540,3 +546,3 @@ if not DEBUG:

def finalize_unix(self) -> None:
def finalize_unix(self):
"""Finalizes options for posix platforms."""

@@ -590,3 +596,3 @@ if self.install_base is not None or self.install_platbase is not None:

def finalize_other(self) -> None:
def finalize_other(self):
"""Finalizes options for non-posix platforms"""

@@ -613,3 +619,3 @@ if self.user:

def select_scheme(self, name) -> None:
def select_scheme(self, name):
_select_scheme(self, name)

@@ -626,3 +632,3 @@

def expand_basedirs(self) -> None:
def expand_basedirs(self):
"""Calls `os.path.expanduser` on install_base, install_platbase and

@@ -632,3 +638,3 @@ root."""

def expand_dirs(self) -> None:
def expand_dirs(self):
"""Calls `os.path.expanduser` on install dirs."""

@@ -644,3 +650,3 @@ self._expand_attrs([

def convert_paths(self, *names) -> None:
def convert_paths(self, *names):
"""Call `convert_path` over `names`."""

@@ -651,3 +657,3 @@ for name in names:

def handle_extra_path(self) -> None:
def handle_extra_path(self):
"""Set `path_file` and `extra_dirs` using `extra_path`."""

@@ -687,3 +693,3 @@ if self.extra_path is None:

def change_roots(self, *names) -> None:
def change_roots(self, *names):
"""Change the install directories pointed by name using root."""

@@ -694,3 +700,3 @@ for name in names:

def create_home_path(self) -> None:
def create_home_path(self):
"""Create directories under ~."""

@@ -697,0 +703,0 @@ if not self.user:

@@ -5,7 +5,4 @@ """distutils.command.sdist

from __future__ import annotations
import os
import sys
from collections.abc import Callable
from distutils import archive_util, dir_util, file_util

@@ -15,3 +12,2 @@ from distutils._log import log

from itertools import filterfalse
from typing import ClassVar

@@ -42,3 +38,3 @@ from ..core import Command

def checking_metadata(self) -> bool:
def checking_metadata(self):
"""Callable used for the check sub-command.

@@ -107,3 +103,3 @@

boolean_options: ClassVar[list[str]] = [
boolean_options = [
'use-defaults',

@@ -117,14 +113,11 @@ 'prune',

help_options: ClassVar[list[tuple[str, str | None, str, Callable[[], object]]]] = [
help_options = [
('help-formats', None, "list available distribution formats", show_formats),
]
negative_opt: ClassVar[dict[str, str]] = {
'no-defaults': 'use-defaults',
'no-prune': 'prune',
}
negative_opt = {'no-defaults': 'use-defaults', 'no-prune': 'prune'}
sub_commands = [('check', checking_metadata)]
READMES: ClassVar[tuple[str, ...]] = ('README', 'README.txt', 'README.rst')
READMES = ('README', 'README.txt', 'README.rst')

@@ -150,7 +143,7 @@ def initialize_options(self):

self.archive_files = None
self.metadata_check = True
self.metadata_check = 1
self.owner = None
self.group = None
def finalize_options(self) -> None:
def finalize_options(self):
if self.manifest is None:

@@ -170,3 +163,3 @@ self.manifest = "MANIFEST"

def run(self) -> None:
def run(self):
# 'filelist' contains the list of files that will make up the

@@ -193,3 +186,3 @@ # manifest

def get_file_list(self) -> None:
def get_file_list(self):
"""Figure out the list of files to include in the source

@@ -235,3 +228,3 @@ distribution, and put it in 'self.filelist'. This might involve

def add_defaults(self) -> None:
def add_defaults(self):
"""Add all the default files to self.filelist:

@@ -351,3 +344,3 @@ - README or README.txt

def read_template(self) -> None:
def read_template(self):
"""Read and parse manifest template file named by self.template.

@@ -382,3 +375,4 @@

self.warn(
f"{template.filename}, line {int(template.current_line)}: {msg}"
"%s, line %d: %s"
% (template.filename, template.current_line, msg)
)

@@ -388,3 +382,3 @@ finally:

def prune_file_list(self) -> None:
def prune_file_list(self):
"""Prune off branches that might slip into the file list as created

@@ -412,3 +406,3 @@ by 'read_template()', but really don't belong there:

def write_manifest(self) -> None:
def write_manifest(self):
"""Write the file list in 'self.filelist' (presumably as filled in

@@ -441,3 +435,3 @@ by 'add_defaults()' and 'read_template()') to the manifest file

def read_manifest(self) -> None:
def read_manifest(self):
"""Read the manifest file (named by 'self.manifest') and use it to

@@ -454,3 +448,3 @@ fill in 'self.filelist', the list of files to include in the source

def make_release_tree(self, base_dir, files) -> None:
def make_release_tree(self, base_dir, files):
"""Create the directory tree that will become the source

@@ -497,3 +491,3 @@ distribution archive. All directories implied by the filenames in

def make_distribution(self) -> None:
def make_distribution(self):
"""Create the source distribution(s). First, we create the release

@@ -536,3 +530,3 @@ tree with 'make_release_tree()'; then, we create all required

def is_comment(line: str) -> bool:
def is_comment(line):
return line.startswith('#')
from __future__ import annotations
from collections.abc import Iterable
from typing import TypeVar
from .py38 import removeprefix
_IterableT = TypeVar("_IterableT", bound="Iterable[str]")
def consolidate_linker_args(args: _IterableT) -> _IterableT | str:
def consolidate_linker_args(args: list[str]) -> list[str] | str:
"""

@@ -18,2 +15,2 @@ Ensure the return value is a string for backward compatibility.

return args
return '-Wl,' + ','.join(arg.removeprefix('-Wl,') for arg in args)
return '-Wl,' + ','.join(removeprefix(arg, '-Wl,') for arg in args)

@@ -9,8 +9,5 @@ """distutils.core

from __future__ import annotations
import os
import sys
import tokenize
from collections.abc import Iterable

@@ -222,3 +219,3 @@ from .cmd import Command

def run_setup(script_name, script_args: Iterable[str] | None = None, stop_after="run"):
def run_setup(script_name, script_args=None, stop_after="run"):
"""Run a setup script in a somewhat controlled environment, and

@@ -225,0 +222,0 @@ return the Distribution instance that drives things. This is useful

@@ -1,27 +0,335 @@

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,
"""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 (
CCompilerError,
CompileError,
DistutilsExecError,
DistutilsPlatformError,
)
from .file_util import write_file
from .sysconfig import get_config_vars
from .unixccompiler import UnixCCompiler
from .version import LooseVersion, suppress_known_deprecation
__all__ = [
'CONFIG_H_NOTOK',
'CONFIG_H_OK',
'CONFIG_H_UNCERTAIN',
'CygwinCCompiler',
'Mingw32CCompiler',
'check_config_h',
'get_msvcr',
'is_cygwincc',
]
def get_msvcr():
"""No longer needed, but kept for backward compatibility."""
return []
CygwinCCompiler = cygwin.Compiler
Mingw32CCompiler = cygwin.MinGW32Compiler
_runtime_library_dirs_msg = (
"Unable to set runtime library search path on Windows, "
"usually indicated by `runtime_library_dirs` parameter to Extension"
)
class CygwinCCompiler(UnixCCompiler):
"""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")
UnixCCompiler.link(
self,
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 Mingw32CCompiler(CygwinCCompiler):
"""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 CCompilerError('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

@@ -28,0 +336,0 @@ """

@@ -35,5 +35,4 @@ """distutils.dir_util

return
result = func(path, *args, **kwargs)
self.add(path.absolute())
return result
return func(path, *args, **kwargs)

@@ -49,3 +48,3 @@ return wrapper

@wrapper
def mkpath(name: pathlib.Path, mode=0o777, verbose=True, dry_run=False) -> None:
def mkpath(name: pathlib.Path, mode=0o777, verbose=True, dry_run=False):
"""Create a directory and any missing ancestor directories.

@@ -58,2 +57,3 @@

If 'verbose' is true, log the directory created.
Return the list of directories actually created.
"""

@@ -63,2 +63,5 @@ if verbose and not name.is_dir():

ancestry = itertools.chain((name,), name.parents)
missing = (path for path in ancestry if not path.is_dir())
try:

@@ -69,3 +72,5 @@ dry_run or name.mkdir(mode=mode, parents=True, exist_ok=True)

return list(map(str, missing))
@mkpath.register

@@ -72,0 +77,0 @@ def _(name: str, *args, **kwargs):

@@ -7,4 +7,2 @@ """distutils.dist

from __future__ import annotations
import contextlib

@@ -17,14 +15,4 @@ import logging

import warnings
from collections.abc import Iterable, MutableMapping
from collections.abc import Iterable
from email import message_from_file
from typing import (
IO,
TYPE_CHECKING,
Any,
ClassVar,
Literal,
TypeVar,
Union,
overload,
)

@@ -44,15 +32,2 @@ from packaging.utils import canonicalize_name, canonicalize_version

if TYPE_CHECKING:
from _typeshed import SupportsWrite
from typing_extensions import TypeAlias
# type-only import because of mutual dependence between these modules
from .cmd import Command
_CommandT = TypeVar("_CommandT", bound="Command")
_OptionsList: TypeAlias = list[
Union[tuple[str, Union[str, None], str, int], tuple[str, Union[str, None], str]]
]
# Regex to define acceptable Distutils command names. This is not *quite*

@@ -65,3 +40,3 @@ # the same as a Python NAME -- I don't allow leading underscores. The fact

def _ensure_list(value: str | Iterable[str], fieldname) -> str | list[str]:
def _ensure_list(value, fieldname):
if isinstance(value, str):

@@ -103,3 +78,3 @@ # a string containing comma separated values is okay. It will

# The fourth entry for verbose means that it can be repeated.
global_options: ClassVar[_OptionsList] = [
global_options = [
('verbose', 'v', "run verbosely (default)", 1),

@@ -114,3 +89,3 @@ ('quiet', 'q', "run quietly (turns verbosity off)"),

# usage of the setup script.
common_usage: ClassVar[str] = """\
common_usage = """\
Common commands: (see '--help-commands' for more)

@@ -123,3 +98,3 @@

# options that are not propagated to the commands
display_options: ClassVar[_OptionsList] = [
display_options = [
('help-commands', None, "list all available commands"),

@@ -151,13 +126,10 @@ ('name', None, "print package name"),

]
display_option_names: ClassVar[list[str]] = [
translate_longopt(x[0]) for x in display_options
]
display_option_names = [translate_longopt(x[0]) for x in display_options]
# negative options are options that exclude other options
negative_opt: ClassVar[dict[str, str]] = {'quiet': 'verbose'}
negative_opt = {'quiet': 'verbose'}
# -- Creation/initialization methods -------------------------------
# Can't Unpack a TypedDict with optional properties, so using Any instead
def __init__(self, attrs: MutableMapping[str, Any] | None = None) -> None: # noqa: C901
def __init__(self, attrs=None): # noqa: C901
"""Construct a new Distribution instance: initialize all the

@@ -178,3 +150,3 @@ attributes of a Distribution, and then use 'attrs' (a dictionary

for attr in self.display_option_names:
setattr(self, attr, False)
setattr(self, attr, 0)

@@ -195,3 +167,3 @@ # Store the distribution meta-data (name, version, author, and so

# for the setup script to override command classes
self.cmdclass: dict[str, type[Command]] = {}
self.cmdclass = {}

@@ -204,3 +176,3 @@ # 'command_packages' is a list of packages in which commands

# searched for. (Always access using get_command_packages().)
self.command_packages: str | list[str] | None = None
self.command_packages = None

@@ -210,4 +182,4 @@ # 'script_name' and 'script_args' are usually set to sys.argv[0]

# not necessarily a setup script run from the command-line.
self.script_name: str | os.PathLike[str] | None = None
self.script_args: list[str] | None = None
self.script_name = None
self.script_args = None

@@ -219,3 +191,3 @@ # 'command_options' is where we store command options between

# command_options = { command_name : { option : (source, value) } }
self.command_options: dict[str, dict[str, tuple[str, str]]] = {}
self.command_options = {}

@@ -231,3 +203,3 @@ # 'dist_files' is the list of (command, pyversion, file) that

# instead.
self.dist_files: list[tuple[str, str, str]] = []
self.dist_files = []

@@ -238,3 +210,3 @@ # These options are really the business of various commands, rather

self.packages = None
self.package_data: dict[str, list[str]] = {}
self.package_data = {}
self.package_dir = None

@@ -256,3 +228,3 @@ self.py_modules = None

# class is a singleton.
self.command_obj: dict[str, Command] = {}
self.command_obj = {}

@@ -269,3 +241,3 @@ # 'have_run' maps command names to boolean values; it keeps track

# '.get()' rather than a straight lookup.
self.have_run: dict[str, bool] = {}
self.have_run = {}

@@ -317,4 +289,2 @@ # Now we'll use the attrs dictionary (ultimately, keyword args from

if self.script_args is not None:
# Coerce any possible iterable from attrs into a list
self.script_args = list(self.script_args)
for arg in self.script_args:

@@ -340,3 +310,3 @@ if not arg.startswith('-'):

def dump_option_dicts(self, header=None, commands=None, indent: str = "") -> None:
def dump_option_dicts(self, header=None, commands=None, indent=""):
from pprint import pformat

@@ -656,3 +626,3 @@

def finalize_options(self) -> None:
def finalize_options(self):
"""Set final values for all the options on the Distribution

@@ -760,3 +730,3 @@ instance, analogous to the .finalize_options() method of Command

def print_command_list(self, commands, header, max_length) -> None:
def print_command_list(self, commands, header, max_length):
"""Print a subset of the list of all commands -- used by

@@ -776,5 +746,5 @@ 'print_commands()'.

print(f" {cmd:<{max_length}} {description}")
print(" %-*s %s" % (max_length, cmd, description))
def print_commands(self) -> None:
def print_commands(self):
"""Print out a help message listing all available commands with a

@@ -846,3 +816,3 @@ description of each. The list is divided into "standard commands"

def get_command_class(self, command: str) -> type[Command]:
def get_command_class(self, command):
"""Return the class that implements the Distutils command named by

@@ -885,11 +855,3 @@ 'command'. First we check the 'cmdclass' dictionary; if the

@overload
def get_command_obj(
self, command: str, create: Literal[True] = True
) -> Command: ...
@overload
def get_command_obj(
self, command: str, create: Literal[False]
) -> Command | None: ...
def get_command_obj(self, command: str, create: bool = True) -> Command | None:
def get_command_obj(self, command, create=True):
"""Return the command object for 'command'. Normally this object

@@ -965,13 +927,3 @@ is cached on a previous call to 'get_command_obj()'; if no command

@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:
def reinitialize_command(self, command, reinit_subcommands=False):
"""Reinitializes a command to the state it was in when first

@@ -1018,6 +970,6 @@ returned by 'get_command_obj()': ie., initialized but not yet

def announce(self, msg, level: int = logging.INFO) -> None:
def announce(self, msg, level=logging.INFO):
log.log(level, msg)
def run_commands(self) -> None:
def run_commands(self):
"""Run each command that was seen on the setup script command line.

@@ -1032,3 +984,3 @@ Uses the list of commands found and cache of command objects

def run_command(self, command: str) -> None:
def run_command(self, command):
"""Do whatever it takes to run a command (including nothing at all,

@@ -1053,24 +1005,24 @@ if the command has already been run). Specifically: if we have

def has_pure_modules(self) -> bool:
def has_pure_modules(self):
return len(self.packages or self.py_modules or []) > 0
def has_ext_modules(self) -> bool:
def has_ext_modules(self):
return self.ext_modules and len(self.ext_modules) > 0
def has_c_libraries(self) -> bool:
def has_c_libraries(self):
return self.libraries and len(self.libraries) > 0
def has_modules(self) -> bool:
def has_modules(self):
return self.has_pure_modules() or self.has_ext_modules()
def has_headers(self) -> bool:
def has_headers(self):
return self.headers and len(self.headers) > 0
def has_scripts(self) -> bool:
def has_scripts(self):
return self.scripts and len(self.scripts) > 0
def has_data_files(self) -> bool:
def has_data_files(self):
return self.data_files and len(self.data_files) > 0
def is_pure(self) -> bool:
def is_pure(self):
return (

@@ -1088,51 +1040,4 @@ self.has_pure_modules()

# DistributionMetadata class, below.
if TYPE_CHECKING:
# Unfortunately this means we need to specify them manually or not expose statically
def _(self) -> None:
self.get_name = self.metadata.get_name
self.get_version = self.metadata.get_version
self.get_fullname = self.metadata.get_fullname
self.get_author = self.metadata.get_author
self.get_author_email = self.metadata.get_author_email
self.get_maintainer = self.metadata.get_maintainer
self.get_maintainer_email = self.metadata.get_maintainer_email
self.get_contact = self.metadata.get_contact
self.get_contact_email = self.metadata.get_contact_email
self.get_url = self.metadata.get_url
self.get_license = self.metadata.get_license
self.get_licence = self.metadata.get_licence
self.get_description = self.metadata.get_description
self.get_long_description = self.metadata.get_long_description
self.get_keywords = self.metadata.get_keywords
self.get_platforms = self.metadata.get_platforms
self.get_classifiers = self.metadata.get_classifiers
self.get_download_url = self.metadata.get_download_url
self.get_requires = self.metadata.get_requires
self.get_provides = self.metadata.get_provides
self.get_obsoletes = self.metadata.get_obsoletes
# Default attributes generated in __init__ from self.display_option_names
help_commands: bool
name: str | Literal[False]
version: str | Literal[False]
fullname: str | Literal[False]
author: str | Literal[False]
author_email: str | Literal[False]
maintainer: str | Literal[False]
maintainer_email: str | Literal[False]
contact: str | Literal[False]
contact_email: str | Literal[False]
url: str | Literal[False]
license: str | Literal[False]
licence: str | Literal[False]
description: str | Literal[False]
long_description: str | Literal[False]
platforms: str | list[str] | Literal[False]
classifiers: str | list[str] | Literal[False]
keywords: str | list[str] | Literal[False]
provides: list[str] | Literal[False]
requires: list[str] | Literal[False]
obsoletes: list[str] | Literal[False]
class DistributionMetadata:

@@ -1167,36 +1072,33 @@ """Dummy class to hold the distribution meta-data: name, version,

def __init__(
self, path: str | bytes | os.PathLike[str] | os.PathLike[bytes] | None = None
) -> None:
def __init__(self, path=None):
if path is not None:
self.read_pkg_file(open(path))
else:
self.name: str | None = None
self.version: str | None = None
self.author: str | None = None
self.author_email: str | None = None
self.maintainer: str | None = None
self.maintainer_email: str | None = None
self.url: str | None = None
self.license: str | None = None
self.description: str | None = None
self.long_description: str | None = None
self.keywords: str | list[str] | None = None
self.platforms: str | list[str] | None = None
self.classifiers: str | list[str] | None = None
self.download_url: str | None = None
self.name = None
self.version = None
self.author = None
self.author_email = None
self.maintainer = None
self.maintainer_email = None
self.url = None
self.license = None
self.description = None
self.long_description = None
self.keywords = None
self.platforms = None
self.classifiers = None
self.download_url = None
# PEP 314
self.provides: str | list[str] | None = None
self.requires: str | list[str] | None = None
self.obsoletes: str | list[str] | None = None
self.provides = None
self.requires = None
self.obsoletes = None
def read_pkg_file(self, file: IO[str]) -> None:
def read_pkg_file(self, file):
"""Reads the metadata values from a file object."""
msg = message_from_file(file)
def _read_field(name: str) -> str | None:
def _read_field(name):
value = msg[name]
if value and value != "UNKNOWN":
return value
return None

@@ -1245,3 +1147,3 @@ def _read_list(name):

def write_pkg_info(self, base_dir: str | os.PathLike[str]) -> None:
def write_pkg_info(self, base_dir):
"""Write the PKG-INFO file into the release tree."""

@@ -1253,3 +1155,3 @@ with open(

def write_pkg_file(self, file: SupportsWrite[str]) -> None:
def write_pkg_file(self, file):
"""Write the PKG-INFO format data to a file object."""

@@ -1300,9 +1202,9 @@ version = '1.0'

def get_name(self) -> str:
def get_name(self):
return self.name or "UNKNOWN"
def get_version(self) -> str:
def get_version(self):
return self.version or "0.0.0"
def get_fullname(self) -> str:
def get_fullname(self):
return self._fullname(self.get_name(), self.get_version())

@@ -1329,24 +1231,24 @@

def get_author(self) -> str | None:
def get_author(self):
return self.author
def get_author_email(self) -> str | None:
def get_author_email(self):
return self.author_email
def get_maintainer(self) -> str | None:
def get_maintainer(self):
return self.maintainer
def get_maintainer_email(self) -> str | None:
def get_maintainer_email(self):
return self.maintainer_email
def get_contact(self) -> str | None:
def get_contact(self):
return self.maintainer or self.author
def get_contact_email(self) -> str | None:
def get_contact_email(self):
return self.maintainer_email or self.author_email
def get_url(self) -> str | None:
def get_url(self):
return self.url
def get_license(self) -> str | None:
def get_license(self):
return self.license

@@ -1356,34 +1258,34 @@

def get_description(self) -> str | None:
def get_description(self):
return self.description
def get_long_description(self) -> str | None:
def get_long_description(self):
return self.long_description
def get_keywords(self) -> str | list[str]:
def get_keywords(self):
return self.keywords or []
def set_keywords(self, value: str | Iterable[str]) -> None:
def set_keywords(self, value):
self.keywords = _ensure_list(value, 'keywords')
def get_platforms(self) -> str | list[str] | None:
def get_platforms(self):
return self.platforms
def set_platforms(self, value: str | Iterable[str]) -> None:
def set_platforms(self, value):
self.platforms = _ensure_list(value, 'platforms')
def get_classifiers(self) -> str | list[str]:
def get_classifiers(self):
return self.classifiers or []
def set_classifiers(self, value: str | Iterable[str]) -> None:
def set_classifiers(self, value):
self.classifiers = _ensure_list(value, 'classifiers')
def get_download_url(self) -> str | None:
def get_download_url(self):
return self.download_url
# PEP 314
def get_requires(self) -> str | list[str]:
def get_requires(self):
return self.requires or []
def set_requires(self, value: Iterable[str]) -> None:
def set_requires(self, value):
import distutils.versionpredicate

@@ -1395,6 +1297,6 @@

def get_provides(self) -> str | list[str]:
def get_provides(self):
return self.provides or []
def set_provides(self, value: Iterable[str]) -> None:
def set_provides(self, value):
value = [v.strip() for v in value]

@@ -1407,6 +1309,6 @@ for v in value:

def get_obsoletes(self) -> str | list[str]:
def get_obsoletes(self):
return self.obsoletes or []
def set_obsoletes(self, value: Iterable[str]) -> None:
def set_obsoletes(self, value):
import distutils.versionpredicate

@@ -1413,0 +1315,0 @@

@@ -8,14 +8,3 @@ """

# 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):

@@ -110,1 +99,28 @@ """The root of all Distutils evil."""

"""Byte compile error."""
# Exception classes used by the CCompiler implementation classes
class CCompilerError(Exception):
"""Some compile/link operation failed."""
class PreprocessError(CCompilerError):
"""Failure to preprocess one or more C/C++ files."""
class CompileError(CCompilerError):
"""Failure to compile one or more C/C++ source files."""
class LibError(CCompilerError):
"""Failure to create a static library from one or more C/C++ object
files."""
class LinkError(CCompilerError):
"""Failure to link one or more C/C++ object files into an executable
or shared library file."""
class UnknownFileError(CCompilerError):
"""Attempt to process an unknown file type."""

@@ -6,7 +6,4 @@ """distutils.extension

from __future__ import annotations
import os
import warnings
from collections.abc import Iterable

@@ -33,10 +30,8 @@ # This class is really only used by the "build_ext" command, so it might

*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.
sources : [string | os.PathLike]
list of source filenames, relative to the distribution root
(where the setup script lives), in Unix form (slash-separated)
for portability. 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]

@@ -96,38 +91,32 @@ list of directories to search for C/C++ header files (in Unix

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,
name,
sources,
include_dirs=None,
define_macros=None,
undef_macros=None,
library_dirs=None,
libraries=None,
runtime_library_dirs=None,
extra_objects=None,
extra_compile_args=None,
extra_link_args=None,
export_symbols=None,
swig_opts=None,
depends=None,
language=None,
optional=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"
raise AssertionError("'name' must be a string") # noqa: TRY004
if not (
isinstance(sources, list)
and all(isinstance(v, (str, os.PathLike)) for v in sources)
):
raise AssertionError(
"'sources' must be a list of strings or PathLike objects."
)
# 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.sources = list(map(os.fspath, sources))
self.include_dirs = include_dirs or []

@@ -134,0 +123,0 @@ self.define_macros = define_macros or []

@@ -11,4 +11,2 @@ """distutils.fancy_getopt

from __future__ import annotations
import getopt

@@ -18,4 +16,3 @@ import re

import sys
from collections.abc import Sequence
from typing import Any
from typing import Any, Sequence

@@ -175,3 +172,4 @@ from .errors import DistutilsArgError, DistutilsGetoptError

raise DistutilsGetoptError(
f"invalid short option '{short}': must a single character or None"
f"invalid short option '{short}': "
"must a single character or None"
)

@@ -227,3 +225,3 @@

def getopt(self, args: Sequence[str] | None = None, object=None): # noqa: C901
def getopt(self, args=None, object=None): # noqa: C901
"""Parse command-line options in args. Store as attributes on object.

@@ -360,5 +358,5 @@

if text:
lines.append(f" --{long:<{max_opt}} {text[0]}")
lines.append(" --%-*s %s" % (max_opt, long, text[0]))
else:
lines.append(f" --{long:<{max_opt}}")
lines.append(" --%-*s " % (max_opt, long))

@@ -370,5 +368,5 @@ # Case 2: we have a short option, so we have to include it

if text:
lines.append(f" --{opt_names:<{max_opt}} {text[0]}")
lines.append(" --%-*s %s" % (max_opt, opt_names, text[0]))
else:
lines.append(f" --{opt_names:<{max_opt}}")
lines.append(" --%-*s" % opt_names)

@@ -386,3 +384,3 @@ for ell in text[1:]:

def fancy_getopt(options, negative_opt, object, args: Sequence[str] | None):
def fancy_getopt(options, negative_opt, object, args):
parser = FancyGetopt(options)

@@ -476,4 +474,4 @@ parser.set_negative_aliases(negative_opt)

for w in (10, 20, 30, 40):
print(f"width: {w}")
print("width: %d" % w)
print("\n".join(wrap_text(text, w)))
print()

@@ -121,3 +121,3 @@ """distutils.file_util

log.debug("not copying %s (output up-to-date)", src)
return (dst, False)
return (dst, 0)

@@ -136,3 +136,3 @@ try:

if dry_run:
return (dst, True)
return (dst, 1)

@@ -151,7 +151,7 @@ # If linking (hard or symbolic), use the appropriate system call

else:
return (dst, True)
return (dst, 1)
elif link == 'sym':
if not (os.path.exists(dst) and os.path.samefile(src, dst)):
os.symlink(src, dst)
return (dst, True)
return (dst, 1)

@@ -171,3 +171,3 @@ # Otherwise (non-Mac, not linking), copy the file contents and

return (dst, True)
return (dst, 1)

@@ -174,0 +174,0 @@

@@ -7,4 +7,2 @@ """distutils.filelist

from __future__ import annotations
import fnmatch

@@ -14,4 +12,2 @@ import functools

import re
from collections.abc import Iterable
from typing import Literal, overload

@@ -38,15 +34,15 @@ from ._log import log

def __init__(self, warn: object = None, debug_print: object = None) -> None:
def __init__(self, warn=None, debug_print=None):
# ignore argument to FileList, but keep them for backwards
# compatibility
self.allfiles: Iterable[str] | None = None
self.files: list[str] = []
self.allfiles = None
self.files = []
def set_allfiles(self, allfiles: Iterable[str]) -> None:
def set_allfiles(self, allfiles):
self.allfiles = allfiles
def findall(self, dir: str | os.PathLike[str] = os.curdir) -> None:
def findall(self, dir=os.curdir):
self.allfiles = findall(dir)
def debug_print(self, msg: object) -> None:
def debug_print(self, msg):
"""Print 'msg' to stdout if the global DEBUG (taken from the

@@ -62,9 +58,9 @@ DISTUTILS_DEBUG environment variable) flag is true.

def append(self, item: str) -> None:
def append(self, item):
self.files.append(item)
def extend(self, items: Iterable[str]) -> None:
def extend(self, items):
self.files.extend(items)
def sort(self) -> None:
def sort(self):
# Not a strict lexical sort!

@@ -78,3 +74,3 @@ sortable_files = sorted(map(os.path.split, self.files))

def remove_duplicates(self) -> None:
def remove_duplicates(self):
# Assumes list has been sorted!

@@ -117,3 +113,3 @@ for i in range(len(self.files) - 1, 0, -1):

def process_template_line(self, line: str) -> None: # noqa: C901
def process_template_line(self, line): # noqa: C901
# Parse the line: split it up, make sure the right number of words

@@ -140,3 +136,6 @@ # is there, and return the relevant words. 'action' is always

log.warning(
"warning: no previously-included files found matching '%s'",
(
"warning: no previously-included files "
"found matching '%s'"
),
pattern,

@@ -207,34 +206,4 @@ )

# 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:
def include_pattern(self, pattern, anchor=True, prefix=None, is_regex=False):
"""Select strings (presumably filenames) from 'self.files' that

@@ -280,34 +249,3 @@ match 'pattern', a Unix-style wildcard (glob) pattern. Patterns

@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:
def exclude_pattern(self, pattern, anchor=True, prefix=None, is_regex=False):
"""Remove strings (presumably filenames) from 'files' that match

@@ -371,3 +309,3 @@ 'pattern'. Other parameters are the same as for

def findall(dir: str | os.PathLike[str] = os.curdir):
def findall(dir=os.curdir):
"""

@@ -374,0 +312,0 @@ Find all files under 'dir' and return the list of full filenames.

@@ -15,4 +15,3 @@ """distutils.spawn

import warnings
from collections.abc import Mapping, MutableSequence
from typing import TYPE_CHECKING, TypeVar, overload
from typing import Mapping

@@ -23,9 +22,3 @@ from ._log import log

if TYPE_CHECKING:
from subprocess import _ENV
_MappingT = TypeVar("_MappingT", bound=Mapping)
def _debug(cmd):

@@ -38,3 +31,3 @@ """

def _inject_macos_ver(env: _MappingT | None) -> _MappingT | dict[str, str | int] | None:
def _inject_macos_ver(env: Mapping[str:str] | None) -> Mapping[str:str] | None:
if platform.system() != 'Darwin':

@@ -50,17 +43,7 @@ return env

@overload
def _resolve(env: None) -> os._Environ[str]: ...
@overload
def _resolve(env: _MappingT) -> _MappingT: ...
def _resolve(env: _MappingT | None) -> _MappingT | os._Environ[str]:
def _resolve(env: Mapping[str:str] | None) -> Mapping[str: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:
def spawn(cmd, search_path=True, verbose=False, dry_run=False, env=None):
"""Run another program, specified as a command list 'cmd', in a new process.

@@ -102,3 +85,3 @@

def find_executable(executable: str, path: str | None = None) -> str | None:
def find_executable(executable, path=None):
"""Tries to find 'executable' in the directories listed in 'path'.

@@ -105,0 +88,0 @@

@@ -12,4 +12,2 @@ """Provide access to Python's configuration information. The specific

from __future__ import annotations
import functools

@@ -21,7 +19,5 @@ import os

import sysconfig
from typing import TYPE_CHECKING, Literal, overload
from jaraco.functools import pass_none
from .ccompiler import CCompiler
from .compat import py39

@@ -31,10 +27,2 @@ from .errors import DistutilsPlatformError

if TYPE_CHECKING:
from typing_extensions import deprecated
else:
def deprecated(message):
return lambda fn: fn
IS_PYPY = '__pypy__' in sys.builtin_module_names

@@ -125,6 +113,6 @@

"""
return f'{sys.version_info.major}.{sys.version_info.minor}'
return '%d.%d' % sys.version_info[:2]
def get_python_inc(plat_specific: bool = False, prefix: str | None = None) -> str:
def get_python_inc(plat_specific=False, prefix=None):
"""Return the directory containing installed Python header files.

@@ -163,2 +151,4 @@

def _get_python_inc_posix(prefix, spec_prefix, plat_specific):
if IS_PYPY and sys.version_info < (3, 8):
return os.path.join(prefix, 'include')
return (

@@ -235,5 +225,3 @@ _get_python_inc_posix_python(plat_specific)

def get_python_lib(
plat_specific: bool = False, standard_lib: bool = False, prefix: str | None = None
) -> str:
def get_python_lib(plat_specific=False, standard_lib=False, prefix=None):
"""Return the directory containing the Python library (standard or

@@ -253,2 +241,10 @@ site additions).

if IS_PYPY and sys.version_info < (3, 8):
# PyPy-specific schema
if prefix is None:
prefix = PREFIX
if standard_lib:
return os.path.join(prefix, "lib-python", sys.version_info.major)
return os.path.join(prefix, 'site-packages')
early_prefix = prefix

@@ -302,3 +298,3 @@

def customize_compiler(compiler: CCompiler) -> None:
def customize_compiler(compiler):
"""Do any platform-specific customization of a CCompiler instance.

@@ -355,3 +351,3 @@

ldcxxshared = _add_flags(ldcxxshared, 'LD')
cflags = os.environ.get('CFLAGS', cflags)
cflags = _add_flags(cflags, 'C')
ldshared = _add_flags(ldshared, 'C')

@@ -391,3 +387,3 @@ cxxflags = os.environ.get('CXXFLAGS', cxxflags)

def get_config_h_filename() -> str:
def get_config_h_filename():
"""Return full pathname of installed pyconfig.h file."""

@@ -397,3 +393,3 @@ return sysconfig.get_config_h_filename()

def get_makefile_filename() -> str:
def get_makefile_filename():
"""Return full pathname of installed Makefile from the Python build."""

@@ -560,7 +556,3 @@ return sysconfig.get_makefile_filename()

@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]:
def get_config_vars(*args):
"""With no arguments, return a dictionary of all configuration

@@ -583,10 +575,3 @@ variables relevant for the current platform. Generally this includes

@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:
def get_config_var(name):
"""Return the value of a single variable using the dictionary

@@ -593,0 +578,0 @@ returned by 'get_config_vars()'. Equivalent to

@@ -11,3 +11,3 @@ """

import shutil
from collections.abc import Sequence
from typing import Sequence

@@ -14,0 +14,0 @@

@@ -11,4 +11,5 @@ """Tests for distutils.command.bdist_rpm."""

import pytest
from test.support import requires_zlib
from .compat.py38 import requires_zlib
SETUP_PY = """\

@@ -15,0 +16,0 @@ from distutils.core import setup

import contextlib
import glob
import importlib
import os.path
import os
import platform

@@ -9,7 +8,5 @@ import re

import site
import subprocess
import sys
import tempfile
import textwrap
import time
from distutils import sysconfig

@@ -26,3 +23,7 @@ from distutils.command.build_ext import build_ext

from distutils.tests import missing_compiler_executable
from distutils.tests.support import TempdirManager, copy_xxmodule_c, fixup_build_ext
from distutils.tests.support import (
TempdirManager,
copy_xxmodule_c,
fixup_build_ext,
)
from io import StringIO

@@ -35,3 +36,3 @@

from .compat import py39 as import_helper
from .compat import py38 as import_helper

@@ -60,6 +61,3 @@

if sys.platform == 'cygwin':
time.sleep(1)
@contextlib.contextmanager

@@ -99,4 +97,3 @@ def safe_extension_import(name, path):

@pytest.mark.parametrize("copy_so", [False])
def test_build_ext(self, copy_so):
def test_build_ext(self):
missing_compiler_executable()

@@ -106,25 +103,2 @@ copy_xxmodule_c(self.tmp_dir)

xx_ext = Extension('xx', [xx_c])
if sys.platform != "win32":
if not copy_so:
xx_ext = Extension(
'xx',
[xx_c],
library_dirs=['/usr/lib'],
libraries=['z'],
runtime_library_dirs=['/usr/lib'],
)
elif sys.platform == 'linux':
libz_so = {
os.path.realpath(name) for name in glob.iglob('/usr/lib*/libz.so*')
}
libz_so = sorted(libz_so, key=lambda lib_path: len(lib_path))
shutil.copyfile(libz_so[-1], '/tmp/libxx_z.so')
xx_ext = Extension(
'xx',
[xx_c],
library_dirs=['/tmp'],
libraries=['xx_z'],
runtime_library_dirs=['/tmp'],
)
dist = Distribution({'name': 'xx', 'ext_modules': [xx_ext]})

@@ -148,10 +122,7 @@ dist.package_dir = self.tmp_dir

with safe_extension_import('xx', self.tmp_dir):
self._test_xx(copy_so)
self._test_xx()
if sys.platform == 'linux' and copy_so:
os.unlink('/tmp/libxx_z.so')
@staticmethod
def _test_xx(copy_so):
import xx # type: ignore[import-not-found] # Module generated for tests
def _test_xx():
import xx

@@ -170,24 +141,2 @@ for attr in ('error', 'foo', 'new', 'roj'):

if sys.platform == 'linux':
so_headers = subprocess.check_output(
["readelf", "-d", xx.__file__], universal_newlines=True
)
import pprint
pprint.pprint(so_headers)
rpaths = [
rpath
for line in so_headers.split("\n")
if "RPATH" in line or "RUNPATH" in line
for rpath in line.split()[2][1:-1].split(":")
]
if not copy_so:
pprint.pprint(rpaths)
# Linked against a library in /usr/lib{,64}
assert "/usr/lib" not in rpaths and "/usr/lib64" not in rpaths
else:
# Linked against a library in /tmp
assert "/tmp" in rpaths
# The import is the real test here
def test_solaris_enable_shared(self):

@@ -411,15 +360,2 @@ dist = Distribution({'name': 'xx'})

def test_export_symbols__init__(self):
# https://github.com/python/cpython/issues/80074
# https://github.com/pypa/setuptools/issues/4826
modules = [
Extension('foo.__init__', ['aaa']),
Extension('föö.__init__', ['uuu']),
]
dist = Distribution({'name': 'xx', 'ext_modules': modules})
cmd = self.build_ext(dist)
cmd.ensure_finalized()
assert cmd.get_export_symbols(modules[0]) == ['PyInit_foo']
assert cmd.get_export_symbols(modules[1]) == ['PyInitU_f_1gaa']
def test_compiler_option(self):

@@ -598,11 +534,10 @@ # cmd.compiler is an option and

# for 10.1 through 10.9.x -> "10n0"
tmpl = '{:02}{:01}0'
target = '%02d%01d0' % target
else:
# for 10.10 and beyond -> "10nn00"
if len(target) >= 2:
tmpl = '{:02}{:02}00'
target = '%02d%02d00' % target
else:
# 11 and later can have no minor version (11 instead of 11.0)
tmpl = '{:02}0000'
target = tmpl.format(*target)
target = '%02d0000' % target
deptarget_ext = Extension(

@@ -609,0 +544,0 @@ 'deptarget',

@@ -43,5 +43,3 @@ """Tests for distutils.command.build."""

# build_scripts is build/scripts-x.x
wanted = os.path.join(
cmd.build_base, f'scripts-{sys.version_info.major}.{sys.version_info.minor}'
)
wanted = os.path.join(cmd.build_base, 'scripts-%d.%d' % sys.version_info[:2])
assert cmd.build_scripts == wanted

@@ -48,0 +46,0 @@

"""Tests for distutils.dir_util."""
import os
import pathlib
import stat
import sys
import unittest.mock as mock

@@ -110,31 +108,6 @@ from distutils import dir_util, errors

"""
with (
mock.patch("os.listdir", side_effect=OSError()),
pytest.raises(errors.DistutilsFileError),
with mock.patch("os.listdir", side_effect=OSError()), pytest.raises(
errors.DistutilsFileError
):
src = self.tempdirs[-1]
dir_util.copy_tree(src, None)
def test_mkpath_exception_uncached(self, monkeypatch, tmp_path):
"""
Caching should not remember failed attempts.
pypa/distutils#304
"""
class FailPath(pathlib.Path):
def mkdir(self, *args, **kwargs):
raise OSError("Failed to create directory")
if sys.version_info < (3, 12):
_flavour = pathlib.Path()._flavour
target = tmp_path / 'foodir'
with pytest.raises(errors.DistutilsFileError):
mkpath(FailPath(target))
assert not target.exists()
mkpath(target)
assert target.exists()

@@ -16,3 +16,2 @@ """Tests for distutils.dist."""

from distutils.tests import support
from typing import ClassVar

@@ -28,3 +27,3 @@ import jaraco.path

user_options: ClassVar[list[tuple[str, str, str]]] = [
user_options = [
("sample-option=", "S", "help text"),

@@ -252,8 +251,2 @@ ]

def test_script_args_list_coercion(self):
d = Distribution(attrs={'script_args': ('build', '--no-user-cfg')})
# make sure script_args is a list even if it started as a different iterable
assert d.script_args == ['build', '--no-user-cfg']
@pytest.mark.skipif(

@@ -260,0 +253,0 @@ 'platform.system() == "Windows"',

@@ -9,5 +9,6 @@ """Tests for distutils.extension."""

import pytest
from test.support.warnings_helper import check_warnings
from .compat.py38 import check_warnings
class TestExtension:

@@ -66,3 +67,3 @@ def test_read_setup_file(self):

# the first argument, which is the name, must be a string
with pytest.raises(TypeError):
with pytest.raises(AssertionError):
Extension(1, [])

@@ -73,6 +74,6 @@ ext = Extension('name', [])

# the second argument, which is the list of files, must
# be an iterable of strings or PathLike objects, and not a string
with pytest.raises(TypeError):
# be a list of strings or PathLike objects
with pytest.raises(AssertionError):
Extension('name', 'file')
with pytest.raises(TypeError):
with pytest.raises(AssertionError):
Extension('name', ['file', 1])

@@ -84,12 +85,2 @@ ext = Extension('name', ['file1', 'file2'])

# any non-string iterable of strings or PathLike objects should work
ext = Extension('name', ('file1', 'file2')) # tuple
assert ext.sources == ['file1', 'file2']
ext = Extension('name', {'file1', 'file2'}) # set
assert sorted(ext.sources) == ['file1', 'file2']
ext = Extension('name', iter(['file1', 'file2'])) # iterator
assert ext.sources == ['file1', 'file2']
ext = Extension('name', [pathlib.Path('file1'), 'file2']) # mixed types
assert ext.sources == ['file1', 'file2']
# others arguments have defaults

@@ -96,0 +87,0 @@ for attr in (

@@ -47,5 +47,4 @@ """Tests for distutils.file_util."""

# see issue 22182
with (
mock.patch("os.rename", side_effect=OSError("wrong", 1)),
pytest.raises(DistutilsFileError),
with mock.patch("os.rename", side_effect=OSError("wrong", 1)), pytest.raises(
DistutilsFileError
):

@@ -57,6 +56,6 @@ jaraco.path.build({self.source: 'spam eggs'})

# see issue 22182
with (
mock.patch("os.rename", side_effect=OSError(errno.EXDEV, "wrong")),
mock.patch("os.unlink", side_effect=OSError("wrong", 1)),
pytest.raises(DistutilsFileError),
with mock.patch(
"os.rename", side_effect=OSError(errno.EXDEV, "wrong")
), mock.patch("os.unlink", side_effect=OSError("wrong", 1)), pytest.raises(
DistutilsFileError
):

@@ -63,0 +62,0 @@ jaraco.path.build({self.source: 'spam eggs'})

@@ -13,3 +13,3 @@ """Tests for distutils.filelist."""

from .compat import py39 as os_helper
from .compat import py38 as os_helper

@@ -16,0 +16,0 @@ MANIFEST_IN = """\

@@ -273,3 +273,3 @@ """Tests for distutils.command.sdist."""

cmd.ensure_finalized()
cmd.metadata_check = False
cmd.metadata_check = 0
cmd.run()

@@ -276,0 +276,0 @@ assert len(self.warnings(caplog.messages, 'warning: check: ')) == 0

@@ -15,3 +15,3 @@ """Tests for distutils.spawn."""

from .compat import py39 as os_helper
from .compat import py38 as os_helper

@@ -77,8 +77,5 @@

env['PATH'] = ''
with (
mock.patch(
'distutils.spawn.os.confstr', return_value=tmp_dir, create=True
),
mock.patch('distutils.spawn.os.defpath', tmp_dir),
):
with mock.patch(
'distutils.spawn.os.confstr', return_value=tmp_dir, create=True
), mock.patch('distutils.spawn.os.defpath', tmp_dir):
rv = find_executable(program)

@@ -95,6 +92,5 @@ assert rv is None

env['PATH'] = os.pathsep
with (
mock.patch('distutils.spawn.os.confstr', return_value='', create=True),
mock.patch('distutils.spawn.os.defpath', ''),
):
with mock.patch(
'distutils.spawn.os.confstr', return_value='', create=True
), mock.patch('distutils.spawn.os.defpath', ''):
rv = find_executable(program)

@@ -113,8 +109,5 @@ assert rv is None

# without confstr
with (
mock.patch(
'distutils.spawn.os.confstr', side_effect=ValueError, create=True
),
mock.patch('distutils.spawn.os.defpath', tmp_dir),
):
with mock.patch(
'distutils.spawn.os.confstr', side_effect=ValueError, create=True
), mock.patch('distutils.spawn.os.defpath', tmp_dir):
rv = find_executable(program)

@@ -124,8 +117,5 @@ assert rv == filename

# with confstr
with (
mock.patch(
'distutils.spawn.os.confstr', return_value=tmp_dir, create=True
),
mock.patch('distutils.spawn.os.defpath', ''),
):
with mock.patch(
'distutils.spawn.os.confstr', return_value=tmp_dir, create=True
), mock.patch('distutils.spawn.os.defpath', ''):
rv = find_executable(program)

@@ -132,0 +122,0 @@ assert rv == filename

@@ -133,5 +133,5 @@ """Tests for distutils.sysconfig."""

assert comp.exes['preprocessor'] == 'env_cpp --env-cppflags'
assert comp.exes['compiler'] == 'env_cc --env-cflags --env-cppflags'
assert comp.exes['compiler'] == 'env_cc --sc-cflags --env-cflags --env-cppflags'
assert comp.exes['compiler_so'] == (
'env_cc --env-cflags --env-cppflags --sc-ccshared'
'env_cc --sc-cflags --env-cflags --env-cppflags --sc-ccshared'
)

@@ -138,0 +138,0 @@ assert (

@@ -56,5 +56,5 @@ """Tests for distutils.version."""

res = StrictVersion(v1)._cmp(object())
assert res is NotImplemented, (
f'cmp({v1}, {v2}) should be NotImplemented, got {res}'
)
assert (
res is NotImplemented
), f'cmp({v1}, {v2}) should be NotImplemented, got {res}'

@@ -79,4 +79,4 @@ def test_cmp(self):

res = LooseVersion(v1)._cmp(object())
assert res is NotImplemented, (
f'cmp({v1}, {v2}) should be NotImplemented, got {res}'
)
assert (
res is NotImplemented
), f'cmp({v1}, {v2}) should be NotImplemented, got {res}'

@@ -136,5 +136,5 @@ """text_file

if isinstance(line, (list, tuple)):
outmsg.append("lines {}-{}: ".format(*line))
outmsg.append("lines %d-%d: " % tuple(line))
else:
outmsg.append(f"line {int(line)}: ")
outmsg.append("line %d: " % line)
outmsg.append(str(msg))

@@ -141,0 +141,0 @@ return "".join(outmsg)

@@ -1,9 +0,402 @@

import importlib
"""distutils.unixccompiler
from .compilers.C import unix
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'
"""
UnixCCompiler = unix.Compiler
from __future__ import annotations
# ensure import of unixccompiler implies ccompiler imported
# (pypa/setuptools#4871)
importlib.import_module('distutils.ccompiler')
import itertools
import os
import re
import shlex
import sys
from . import sysconfig
from ._log import log
from ._macos_compat import compiler_fixup
from ._modified import newer
from .ccompiler import CCompiler, gen_lib_options, gen_preprocess_options
from .compat import consolidate_linker_args
from .errors import CompileError, DistutilsExecError, 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 UnixCCompiler(CCompiler):
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 preprocess(
self,
source,
output_file=None,
macros=None,
include_dirs=None,
extra_preargs=None,
extra_postargs=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,
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,
):
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 == CCompiler.EXECUTABLE
linker = (
self.linker_exe
if building_exe
else (
self.linker_so_cxx if target_lang == "c++" else self.linker_so
)
)[:]
if target_lang == "c++" 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)
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]:
# 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)

@@ -19,4 +19,2 @@ """distutils.util

import tempfile
from collections.abc import Callable, Iterable, Mapping
from typing import TYPE_CHECKING, AnyStr

@@ -30,9 +28,4 @@ from jaraco.functools import pass_none

if TYPE_CHECKING:
from typing_extensions import TypeVarTuple, Unpack
_Ts = TypeVarTuple("_Ts")
def get_host_platform() -> str:
def get_host_platform():
"""

@@ -46,8 +39,16 @@ Return a string that identifies the current platform. Use this

# even with older Python versions when distutils was split out.
# Now it delegates to stdlib sysconfig.
# Now it delegates to stdlib sysconfig, but maintains compatibility.
if sys.version_info < (3, 9):
if os.name == "posix" and hasattr(os, 'uname'):
osname, host, release, version, machine = os.uname()
if osname[:3] == "aix":
from .compat.py38 import aix_platform
return aix_platform(osname, version, release)
return sysconfig.get_platform()
def get_platform() -> str:
def get_platform():
if os.name == 'nt':

@@ -121,3 +122,3 @@ TARGET_TO_PLAT = {

def split_version(s: str) -> list[int]:
def split_version(s):
"""Convert a dot-separated string into a list of numbers for comparisons"""

@@ -128,3 +129,3 @@ return [int(n) for n in s.split('.')]

@pass_none
def convert_path(pathname: str | os.PathLike[str]) -> str:
def convert_path(pathname: str | os.PathLike) -> str:
r"""

@@ -147,5 +148,3 @@ Allow for pathlib.Path inputs, coax to a native path string.

def change_root(
new_root: AnyStr | os.PathLike[AnyStr], pathname: AnyStr | os.PathLike[AnyStr]
) -> AnyStr:
def change_root(new_root, pathname):
"""Return 'pathname' with 'new_root' prepended. If 'pathname' is

@@ -172,3 +171,3 @@ relative, this is equivalent to "os.path.join(new_root,pathname)".

@functools.lru_cache
def check_environ() -> None:
def check_environ():
"""Ensure that 'os.environ' has all the environment variables we

@@ -195,3 +194,3 @@ guarantee that users can use in config files, command-line options,

def subst_vars(s, local_vars: Mapping[str, object]) -> str:
def subst_vars(s, local_vars):
"""

@@ -235,3 +234,3 @@ Perform variable substitution on 'string'.

def grok_environment_error(exc: object, prefix: str = "error: ") -> str:
def grok_environment_error(exc, prefix="error: "):
# Function kept for backward compatibility.

@@ -254,3 +253,3 @@ # Used to try clever things with EnvironmentErrors,

def split_quoted(s: str) -> list[str]:
def split_quoted(s):
"""Split a string up according to Unix shell-like rules for quotes and

@@ -302,3 +301,3 @@ backslashes. In short: words are delimited by spaces, as long as those

else:
raise RuntimeError(f"this can't happen (bad char '{s[end]}')")
raise RuntimeError("this can't happen (bad char '%c')" % s[end])

@@ -322,17 +321,10 @@ if m is None:

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
def execute(func, args, msg=None, verbose=False, dry_run=False):
"""Perform some action that affects the outside world (eg. by
writing to the filesystem). Such actions are special because they
are disabled by the 'dry_run' flag. This method takes care of all
that bureaucracy for you; all you have to do is supply the
function to call and an argument tuple for it (to embody the
"external action" being performed) and an optional message to
emit.
"external action" being performed), and an optional message to
print.
"""

@@ -349,3 +341,3 @@ if msg is None:

def strtobool(val: str) -> bool:
def strtobool(val):
"""Convert a string representation of truth to true (1) or false (0).

@@ -359,5 +351,5 @@

if val in ('y', 'yes', 't', 'true', 'on', '1'):
return True
return 1
elif val in ('n', 'no', 'f', 'false', 'off', '0'):
return False
return 0
else:

@@ -368,11 +360,11 @@ 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:
py_files,
optimize=0,
force=False,
prefix=None,
base_dir=None,
verbose=True,
dry_run=False,
direct=None,
):
"""Byte-compile a collection of Python source files to .pyc

@@ -507,3 +499,3 @@ files in a __pycache__ subdirectory. 'py_files' is a list

def rfc822_escape(header: str) -> str:
def rfc822_escape(header):
"""Return a version of the string escaped for inclusion in an

@@ -523,3 +515,3 @@ RFC-822 header, by ensuring there are 8 spaces space after each newline.

def is_mingw() -> bool:
def is_mingw():
"""Returns True if the current platform is mingw.

@@ -531,6 +523,1 @@

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'))

@@ -56,3 +56,4 @@ #

warnings.warn(
"distutils Version classes are deprecated. Use packaging.version instead.",
"distutils Version classes are deprecated. "
"Use packaging.version instead.",
DeprecationWarning,

@@ -59,0 +60,0 @@ stacklevel=2,

@@ -1,3 +0,229 @@

from .compilers.C import zos
"""distutils.zosccompiler
zOSCCompiler = zos.Compiler
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 CompileError, DistutilsExecError
from .unixccompiler import UnixCCompiler
_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 zOSCCompiler(UnixCCompiler):
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,
)

@@ -18,6 +18,2 @@ import functools

the pattern match.
This function is deprecated in favor of importlib_metadata 8.7 and
Python 3.14 importlib.metadata, which validates entry points on
construction.
"""

@@ -24,0 +20,0 @@ try:

@@ -32,3 +32,3 @@ """

if spec is None:
raise ImportError(f"Can't find {module}")
raise ImportError("Can't find %s" % module)
if not spec.has_location and hasattr(spec, 'submodule_search_locations'):

@@ -80,3 +80,3 @@ spec = importlib.util.spec_from_loader('__init__.py', spec.loader)

if not spec:
raise ImportError(f"Can't find {module}")
raise ImportError("Can't find %s" % module)
return spec.loader.get_code(module)

@@ -88,3 +88,3 @@

if not spec:
raise ImportError(f"Can't find {module}")
raise ImportError("Can't find %s" % module)
return module_from_spec(spec)

@@ -9,2 +9,5 @@ import sys

import importlib.resources as resources # noqa: F401
if sys.version_info < (3, 9):
import importlib_resources as resources # pragma: no cover
else:
import importlib.resources as resources # noqa: F401

@@ -7,3 +7,2 @@ """

import re
from typing import TYPE_CHECKING

@@ -40,2 +39,3 @@ import packaging

"""
# See pkg_resources.safe_name
return _UNSAFE_NAME_CHARS.sub("-", component)

@@ -85,2 +85,3 @@

"""
# See pkg_resources._forgiving_version
try:

@@ -153,29 +154,1 @@ return safe_version(version)

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

@@ -6,3 +6,3 @@ from __future__ import annotations

import sys
from typing import TYPE_CHECKING, TypeVar, Union
from typing import TYPE_CHECKING, Union

@@ -14,4 +14,6 @@ from more_itertools import unique_everseen

StrPath: TypeAlias = Union[str, os.PathLike[str]] # Same as _typeshed.StrPath
StrPathT = TypeVar("StrPathT", bound=Union[str, os.PathLike[str]])
StrPath: TypeAlias = Union[str, os.PathLike[str]] # Same as _typeshed.StrPath
else:
# Python 3.8 support
StrPath: TypeAlias = Union[str, os.PathLike]

@@ -44,16 +46,7 @@

def _cygwin_patch(filename: StrPath): # pragma: nocover
"""
Contrary to POSIX 2008, on Cygwin, getcwd (3) contains
symlink components. Using
os.path.abspath() works around this limitation. A fix in os.getcwd()
would probably better, in Cygwin even more so, except
that this seems to be by design...
"""
return os.path.abspath(filename) if sys.platform == 'cygwin' else filename
def normpath(filename: StrPath) -> str:
"""Normalize a file/dir name for comparison purposes."""
return os.path.normcase(os.path.realpath(os.path.normpath(_cygwin_patch(filename))))
# 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)))

@@ -60,0 +53,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
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, TypeVar, Union, overload

@@ -40,4 +39,4 @@ import jaraco.text as text

"""
Parse requirements.
Replacement for ``pkg_resources.parse_requirements`` that uses ``packaging``.
"""
return map(parser, parse_strings(strs))

@@ -9,3 +9,3 @@ # This file is dual licensed under the terms of the Apache License, Version

__version__ = "24.2"
__version__ = "24.1"

@@ -16,2 +16,2 @@ __author__ = "Donald Stufft and individual contributors"

__license__ = "BSD-2-Clause or Apache-2.0"
__copyright__ = f"2014 {__author__}"
__copyright__ = "2014 %s" % __author__

@@ -51,4 +51,4 @@ """

ident = self._read("16B")
except struct.error as e:
raise ELFInvalid("unable to parse identification") from e
except struct.error:
raise ELFInvalid("unable to parse identification")
magic = bytes(ident[:4])

@@ -71,7 +71,7 @@ if magic != b"\x7fELF":

}[(self.capacity, self.encoding)]
except KeyError as e:
except KeyError:
raise ELFInvalid(
f"unrecognized capacity ({self.capacity}) or "
f"encoding ({self.encoding})"
) from e
)

@@ -78,0 +78,0 @@ try:

@@ -167,3 +167,2 @@ from __future__ import annotations

RuntimeWarning,
stacklevel=2,
)

@@ -170,0 +169,0 @@ return -1, -1

@@ -21,5 +21,5 @@ # This file is dual licensed under the terms of the Apache License, Version

"InvalidMarker",
"Marker",
"UndefinedComparison",
"UndefinedEnvironmentName",
"Marker",
"default_environment",

@@ -236,3 +236,3 @@ ]

def format_full_version(info: sys._version_info) -> str:
version = f"{info.major}.{info.minor}.{info.micro}"
version = "{0.major}.{0.minor}.{0.micro}".format(info)
kind = info.releaselevel

@@ -314,2 +314,8 @@ if kind != "final":

current_environment["extra"] = ""
# Work around platform.python_version() returning something that is not PEP 440
# compliant for non-tagged Python builds. We preserve default_environment()'s
# behavior of returning platform.python_version() verbatim, and leave it to the
# caller to provide a syntactically valid version if they want to override it.
if current_environment["python_full_version"].endswith("+"):
current_environment["python_full_version"] += "local"
if environment is not None:

@@ -322,14 +328,2 @@ current_environment.update(environment)

return _evaluate_markers(
self._markers, _repair_python_full_version(current_environment)
)
def _repair_python_full_version(env: dict[str, str]) -> dict[str, str]:
"""
Work around platform.python_version() returning something that is not PEP 440
compliant for non-tagged Python builds.
"""
if env["python_full_version"].endswith("+"):
env["python_full_version"] += "local"
return env
return _evaluate_markers(self._markers, current_environment)

@@ -8,4 +8,2 @@ from __future__ import annotations

import email.policy
import pathlib
import sys
import typing

@@ -21,5 +19,4 @@ from typing import (

from . import licenses, requirements, specifiers, utils
from . import requirements, specifiers, utils
from . import version as version_module
from .licenses import NormalizedLicenseExpression

@@ -29,5 +26,5 @@ T = typing.TypeVar("T")

if sys.version_info >= (3, 11): # pragma: no cover
ExceptionGroup = ExceptionGroup
else: # pragma: no cover
try:
ExceptionGroup
except NameError: # pragma: no cover

@@ -51,3 +48,6 @@ class ExceptionGroup(Exception):

else: # pragma: no cover
ExceptionGroup = ExceptionGroup
class InvalidMetadata(ValueError):

@@ -135,7 +135,3 @@ """A metadata field contains invalid data."""

# Metadata 2.4 - PEP 639
license_expression: str
license_files: list[str]
_STRING_FIELDS = {

@@ -149,3 +145,2 @@ "author",

"license",
"license_expression",
"maintainer",

@@ -163,3 +158,2 @@ "maintainer_email",

"dynamic",
"license_files",
"obsoletes",

@@ -183,3 +177,3 @@ "obsoletes_dist",

def _parse_keywords(data: str) -> list[str]:
"""Split a string of comma-separated keywords into a list of keywords."""
"""Split a string of comma-separate keyboards into a list of keywords."""
return [k.strip() for k in data.split(",")]

@@ -233,4 +227,3 @@

if isinstance(source, str):
payload = msg.get_payload()
assert isinstance(payload, str)
payload: str = msg.get_payload()
return payload

@@ -240,8 +233,7 @@ # If our source is a bytes, then we're managing the encoding and we need

else:
bpayload = msg.get_payload(decode=True)
assert isinstance(bpayload, bytes)
bpayload: bytes = msg.get_payload(decode=True)
try:
return bpayload.decode("utf8", "strict")
except UnicodeDecodeError as exc:
raise ValueError("payload in an invalid encoding") from exc
except UnicodeDecodeError:
raise ValueError("payload in an invalid encoding")

@@ -272,4 +264,2 @@

"license": "license",
"license-expression": "license_expression",
"license-file": "license_files",
"maintainer": "maintainer",

@@ -450,3 +440,3 @@ "maintainer-email": "maintainer_email",

unparsed.setdefault("description", []).append(
parsed.get_payload(decode=isinstance(data, bytes)) # type: ignore[call-overload]
parsed.get_payload(decode=isinstance(data, bytes))
)

@@ -478,4 +468,4 @@ else:

# Keep the two values in sync.
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3"]
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3"]

@@ -561,3 +551,3 @@ _REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"])

f"{value!r} is invalid for {{field}}", cause=exc
) from exc
)
else:

@@ -574,3 +564,3 @@ return value

f"{value!r} is invalid for {{field}}", cause=exc
) from exc
)

@@ -619,8 +609,6 @@ def _process_summary(self, value: str) -> str:

raise self._invalid_metadata(
f"{dynamic_field!r} is not allowed as a dynamic field"
f"{value!r} is not allowed as a dynamic field"
)
elif dynamic_field not in _EMAIL_TO_RAW_MAPPING:
raise self._invalid_metadata(
f"{dynamic_field!r} is not a valid dynamic field"
)
raise self._invalid_metadata(f"{value!r} is not a valid dynamic field")
return list(map(str.lower, value))

@@ -639,3 +627,3 @@

f"{name!r} is invalid for {{field}}", cause=exc
) from exc
)
else:

@@ -650,3 +638,3 @@ return normalized_names

f"{value!r} is invalid for {{field}}", cause=exc
) from exc
)

@@ -662,46 +650,7 @@ def _process_requires_dist(

except requirements.InvalidRequirement as exc:
raise self._invalid_metadata(
f"{req!r} is invalid for {{field}}", cause=exc
) from exc
raise self._invalid_metadata(f"{req!r} is invalid for {{field}}", cause=exc)
else:
return reqs
def _process_license_expression(
self, value: str
) -> NormalizedLicenseExpression | None:
try:
return licenses.canonicalize_license_expression(value)
except ValueError as exc:
raise self._invalid_metadata(
f"{value!r} is invalid for {{field}}", cause=exc
) from exc
def _process_license_files(self, value: list[str]) -> list[str]:
paths = []
for path in value:
if ".." in path:
raise self._invalid_metadata(
f"{path!r} is invalid for {{field}}, "
"parent directory indicators are not allowed"
)
if "*" in path:
raise self._invalid_metadata(
f"{path!r} is invalid for {{field}}, paths must be resolved"
)
if (
pathlib.PurePosixPath(path).is_absolute()
or pathlib.PureWindowsPath(path).is_absolute()
):
raise self._invalid_metadata(
f"{path!r} is invalid for {{field}}, paths must be relative"
)
if pathlib.PureWindowsPath(path).as_posix() != path:
raise self._invalid_metadata(
f"{path!r} is invalid for {{field}}, "
"paths must use '/' delimiter"
)
paths.append(path)
return paths
class Metadata:

@@ -761,4 +710,4 @@ """Representation of distribution metadata.

field,
f"{field} introduced in metadata version "
f"{field_metadata_version}, not {metadata_version}",
"{field} introduced in metadata version "
"{field_metadata_version}, not {metadata_version}",
)

@@ -807,4 +756,2 @@ exceptions.append(exc)

(required; validated to be a valid metadata version)"""
# `name` is not normalized/typed to NormalizedName so as to provide access to
# the original/raw name.
name: _Validator[str] = _Validator()

@@ -847,8 +794,2 @@ """:external:ref:`core-metadata-name`

""":external:ref:`core-metadata-license`"""
license_expression: _Validator[NormalizedLicenseExpression | None] = _Validator(
added="2.4"
)
""":external:ref:`core-metadata-license-expression`"""
license_files: _Validator[list[str] | None] = _Validator(added="2.4")
""":external:ref:`core-metadata-license-file`"""
classifiers: _Validator[list[str] | None] = _Validator(added="1.1")

@@ -855,0 +796,0 @@ """:external:ref:`core-metadata-classifier`"""

@@ -237,3 +237,3 @@ # This file is dual licensed under the terms of the Apache License, Version

if not match:
raise InvalidSpecifier(f"Invalid specifier: {spec!r}")
raise InvalidSpecifier(f"Invalid specifier: '{spec}'")

@@ -260,3 +260,3 @@ self._spec: tuple[str, str] = (

operator, version = self._spec
if operator in ["==", ">=", "<=", "~=", "===", ">", "<"]:
if operator in ["==", ">=", "<=", "~=", "==="]:
# The == specifier can include a trailing .*, if it does we

@@ -699,7 +699,3 @@ # want to remove before parsing.

def __init__(
self,
specifiers: str | Iterable[Specifier] = "",
prereleases: bool | None = None,
) -> None:
def __init__(self, specifiers: str = "", prereleases: bool | None = None) -> None:
"""Initialize a SpecifierSet instance.

@@ -710,4 +706,2 @@

specifiers which will be parsed and normalized before use.
May also be an iterable of ``Specifier`` instances, which will be used
as is.
:param prereleases:

@@ -723,13 +717,8 @@ This tells the SpecifierSet if it should accept prerelease versions if

if isinstance(specifiers, str):
# Split on `,` to break each individual specifier into its own item, and
# strip each item to remove leading/trailing whitespace.
split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
# Split on `,` to break each individual specifier into it's own item, and
# strip each item to remove leading/trailing whitespace.
split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
# Make each individual specifier a Specifier and save in a frozen set
# for later.
self._specs = frozenset(map(Specifier, split_specifiers))
else:
# Save the supplied specifiers in a frozen set.
self._specs = frozenset(specifiers)
# Make each individual specifier a Specifier and save in a frozen set for later.
self._specs = frozenset(map(Specifier, split_specifiers))

@@ -736,0 +725,0 @@ # Store our prereleases value so we can use it later to determine if

@@ -28,3 +28,3 @@ # This file is dual licensed under the terms of the Apache License, Version

PythonVersion = Sequence[int]
AppleVersion = Tuple[int, int]
MacVersion = Tuple[int, int]

@@ -51,3 +51,3 @@ INTERPRETER_SHORT_NAMES: dict[str, str] = {

__slots__ = ["_abi", "_hash", "_interpreter", "_platform"]
__slots__ = ["_interpreter", "_abi", "_platform", "_hash"]

@@ -240,4 +240,5 @@ def __init__(self, interpreter: str, abi: str, platform: str) -> None:

for platform_ in platforms:
version = _version_nodot((python_version[0], minor_version))
interpreter = f"cp{version}"
interpreter = "cp{version}".format(
version=_version_nodot((python_version[0], minor_version))
)
yield Tag(interpreter, "abi3", platform_)

@@ -368,3 +369,3 @@

def _mac_binary_formats(version: AppleVersion, cpu_arch: str) -> list[str]:
def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]:
formats = [cpu_arch]

@@ -402,3 +403,3 @@ if cpu_arch == "x86_64":

def mac_platforms(
version: AppleVersion | None = None, arch: str | None = None
version: MacVersion | None = None, arch: str | None = None
) -> Iterator[str]:

@@ -415,3 +416,3 @@ """

if version is None:
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
if version == (10, 16):

@@ -432,3 +433,3 @@ # When built against an older macOS SDK, Python will report macOS 10.16

).stdout
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
else:

@@ -444,8 +445,9 @@ version = version

# "minor" version number. The major version was always 10.
major_version = 10
for minor_version in range(version[1], -1, -1):
compat_version = major_version, minor_version
compat_version = 10, minor_version
binary_formats = _mac_binary_formats(compat_version, arch)
for binary_format in binary_formats:
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
yield "macosx_{major}_{minor}_{binary_format}".format(
major=10, minor=minor_version, binary_format=binary_format
)

@@ -455,8 +457,9 @@ if version >= (11, 0):

# number. The minor versions are now the midyear updates.
minor_version = 0
for major_version in range(version[0], 10, -1):
compat_version = major_version, minor_version
compat_version = major_version, 0
binary_formats = _mac_binary_formats(compat_version, arch)
for binary_format in binary_formats:
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
yield "macosx_{major}_{minor}_{binary_format}".format(
major=major_version, minor=0, binary_format=binary_format
)

@@ -471,73 +474,23 @@ if version >= (11, 0):

# that version of macOS.
major_version = 10
if arch == "x86_64":
for minor_version in range(16, 3, -1):
compat_version = major_version, minor_version
compat_version = 10, minor_version
binary_formats = _mac_binary_formats(compat_version, arch)
for binary_format in binary_formats:
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
yield "macosx_{major}_{minor}_{binary_format}".format(
major=compat_version[0],
minor=compat_version[1],
binary_format=binary_format,
)
else:
for minor_version in range(16, 3, -1):
compat_version = major_version, minor_version
compat_version = 10, minor_version
binary_format = "universal2"
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
yield "macosx_{major}_{minor}_{binary_format}".format(
major=compat_version[0],
minor=compat_version[1],
binary_format=binary_format,
)
def ios_platforms(
version: AppleVersion | None = None, multiarch: str | None = None
) -> Iterator[str]:
"""
Yields the platform tags for an iOS system.
:param version: A two-item tuple specifying the iOS version to generate
platform tags for. Defaults to the current iOS version.
:param multiarch: The CPU architecture+ABI to generate platform tags for -
(the value used by `sys.implementation._multiarch` e.g.,
`arm64_iphoneos` or `x84_64_iphonesimulator`). Defaults to the current
multiarch value.
"""
if version is None:
# if iOS is the current platform, ios_ver *must* be defined. However,
# it won't exist for CPython versions before 3.13, which causes a mypy
# error.
_, release, _, _ = platform.ios_ver() # type: ignore[attr-defined, unused-ignore]
version = cast("AppleVersion", tuple(map(int, release.split(".")[:2])))
if multiarch is None:
multiarch = sys.implementation._multiarch
multiarch = multiarch.replace("-", "_")
ios_platform_template = "ios_{major}_{minor}_{multiarch}"
# Consider any iOS major.minor version from the version requested, down to
# 12.0. 12.0 is the first iOS version that is known to have enough features
# to support CPython. Consider every possible minor release up to X.9. There
# highest the minor has ever gone is 8 (14.8 and 15.8) but having some extra
# candidates that won't ever match doesn't really hurt, and it saves us from
# having to keep an explicit list of known iOS versions in the code. Return
# the results descending order of version number.
# If the requested major version is less than 12, there won't be any matches.
if version[0] < 12:
return
# Consider the actual X.Y version that was requested.
yield ios_platform_template.format(
major=version[0], minor=version[1], multiarch=multiarch
)
# Consider every minor version from X.0 to the minor version prior to the
# version requested by the platform.
for minor in range(version[1] - 1, -1, -1):
yield ios_platform_template.format(
major=version[0], minor=minor, multiarch=multiarch
)
for major in range(version[0] - 1, 11, -1):
for minor in range(9, -1, -1):
yield ios_platform_template.format(
major=major, minor=minor, multiarch=multiarch
)
def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:

@@ -572,4 +525,2 @@ linux = _normalize_string(sysconfig.get_platform())

return mac_platforms()
elif platform.system() == "iOS":
return ios_platforms()
elif platform.system() == "Linux":

@@ -576,0 +527,0 @@ return _linux_platforms()

@@ -7,3 +7,2 @@ # This file is dual licensed under the terms of the Apache License, Version

import functools
import re

@@ -13,3 +12,3 @@ from typing import NewType, Tuple, Union, cast

from .tags import Tag, parse_tag
from .version import InvalidVersion, Version, _TrimmedRelease
from .version import InvalidVersion, Version

@@ -60,3 +59,2 @@ BuildTag = Union[Tuple[()], Tuple[int, str]]

@functools.singledispatch
def canonicalize_version(

@@ -66,33 +64,46 @@ version: Version | str, *, strip_trailing_zero: bool = True

"""
Return a canonical form of a version as a string.
This is very similar to Version.__str__, but has one subtle difference
with the way it handles the release segment.
"""
if isinstance(version, str):
try:
parsed = Version(version)
except InvalidVersion:
# Legacy versions cannot be normalized
return version
else:
parsed = version
>>> canonicalize_version('1.0.1')
'1.0.1'
parts = []
Per PEP 625, versions may have multiple canonical forms, differing
only by trailing zeros.
# Epoch
if parsed.epoch != 0:
parts.append(f"{parsed.epoch}!")
>>> canonicalize_version('1.0.0')
'1'
>>> canonicalize_version('1.0.0', strip_trailing_zero=False)
'1.0.0'
# Release segment
release_segment = ".".join(str(x) for x in parsed.release)
if strip_trailing_zero:
# NB: This strips trailing '.0's to normalize
release_segment = re.sub(r"(\.0)+$", "", release_segment)
parts.append(release_segment)
Invalid versions are returned unaltered.
# Pre-release
if parsed.pre is not None:
parts.append("".join(str(x) for x in parsed.pre))
>>> canonicalize_version('foo bar baz')
'foo bar baz'
"""
return str(_TrimmedRelease(str(version)) if strip_trailing_zero else version)
# Post-release
if parsed.post is not None:
parts.append(f".post{parsed.post}")
# Development release
if parsed.dev is not None:
parts.append(f".dev{parsed.dev}")
@canonicalize_version.register
def _(version: str, *, strip_trailing_zero: bool = True) -> str:
try:
parsed = Version(version)
except InvalidVersion:
# Legacy versions cannot be normalized
return version
return canonicalize_version(parsed, strip_trailing_zero=strip_trailing_zero)
# Local version segment
if parsed.local is not None:
parts.append(f"+{parsed.local}")
return "".join(parts)
def parse_wheel_filename(

@@ -103,3 +114,3 @@ filename: str,

raise InvalidWheelFilename(
f"Invalid wheel filename (extension must be '.whl'): {filename!r}"
f"Invalid wheel filename (extension must be '.whl'): {filename}"
)

@@ -111,3 +122,3 @@

raise InvalidWheelFilename(
f"Invalid wheel filename (wrong number of parts): {filename!r}"
f"Invalid wheel filename (wrong number of parts): {filename}"
)

@@ -119,3 +130,3 @@

if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None:
raise InvalidWheelFilename(f"Invalid project name: {filename!r}")
raise InvalidWheelFilename(f"Invalid project name: {filename}")
name = canonicalize_name(name_part)

@@ -127,3 +138,3 @@

raise InvalidWheelFilename(
f"Invalid wheel filename (invalid version): {filename!r}"
f"Invalid wheel filename (invalid version): {filename}"
) from e

@@ -136,3 +147,3 @@

raise InvalidWheelFilename(
f"Invalid build number: {build_part} in {filename!r}"
f"Invalid build number: {build_part} in '{filename}'"
)

@@ -154,3 +165,3 @@ build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2)))

f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):"
f" {filename!r}"
f" {filename}"
)

@@ -162,3 +173,3 @@

if not sep:
raise InvalidSdistFilename(f"Invalid sdist filename: {filename!r}")
raise InvalidSdistFilename(f"Invalid sdist filename: {filename}")

@@ -171,5 +182,5 @@ name = canonicalize_name(name_part)

raise InvalidSdistFilename(
f"Invalid sdist filename (invalid version): {filename!r}"
f"Invalid sdist filename (invalid version): {filename}"
) from e
return (name, version)

@@ -18,3 +18,3 @@ # This file is dual licensed under the terms of the Apache License, Version

__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "parse"]
__all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"]

@@ -203,3 +203,3 @@ LocalType = Tuple[Union[int, str], ...]

if not match:
raise InvalidVersion(f"Invalid version: {version!r}")
raise InvalidVersion(f"Invalid version: '{version}'")

@@ -237,3 +237,3 @@ # Store the parsed out pieces of the version

def __str__(self) -> str:
"""A string representation of the version that can be round-tripped.
"""A string representation of the version that can be rounded-tripped.

@@ -356,4 +356,4 @@ >>> str(Version("1.0a5"))

'1.2.3'
>>> Version("1!1.2.3dev1+abc").public
'1!1.2.3.dev1'
>>> Version("1.2.3+abc.dev1").public
'1.2.3'
"""

@@ -370,3 +370,3 @@ return str(self).split("+", 1)[0]

'1.2.3'
>>> Version("1!1.2.3dev1+abc").base_version
>>> Version("1!1.2.3+abc.dev1").base_version
'1!1.2.3'

@@ -459,19 +459,2 @@

class _TrimmedRelease(Version):
@property
def release(self) -> tuple[int, ...]:
"""
Release segment without any trailing zeros.
>>> _TrimmedRelease('1.0.0').release
(1,)
>>> _TrimmedRelease('0.0').release
(0,)
"""
rel = super().release
nonzeros = (index for index, val in enumerate(rel) if val)
last_nonzero = max(nonzeros, default=0)
return rel[: last_nonzero + 1]
def _parse_letter_version(

@@ -502,5 +485,3 @@ letter: str | None, number: str | bytes | SupportsInt | None

return letter, int(number)
assert not letter
if number:
if not letter and number:
# We assume if we are given a number, but we are not given a letter

@@ -507,0 +488,0 @@ # then this is using the implicit post release syntax (e.g. 1.0-1)

from __future__ import annotations
__version__ = "0.45.1"
__version__ = "0.43.0"

@@ -8,7 +8,7 @@ # copied from setuptools.logging, omitting monkeypatching

def _not_warning(record: logging.LogRecord) -> bool:
def _not_warning(record):
return record.levelno < logging.WARNING
def configure() -> None:
def configure():
"""

@@ -15,0 +15,0 @@ Configure logging to emit warning and above to stderr

@@ -1,26 +0,595 @@

from typing import TYPE_CHECKING
from warnings import warn
"""
Create a wheel (.whl) distribution.
warn(
"The 'wheel' package is no longer the canonical location of the 'bdist_wheel' "
"command, and will be removed in a future release. Please update to setuptools "
"v70.1 or later which contains an integrated version of this command.",
DeprecationWarning,
stacklevel=1,
)
A wheel is a built archive format.
"""
if TYPE_CHECKING:
from ._bdist_wheel import bdist_wheel as bdist_wheel
else:
from __future__ import annotations
import os
import re
import shutil
import stat
import struct
import sys
import sysconfig
import warnings
from email.generator import BytesGenerator, Generator
from email.policy import EmailPolicy
from glob import iglob
from shutil import rmtree
from zipfile import ZIP_DEFLATED, ZIP_STORED
import setuptools
from setuptools import Command
from . import __version__ as wheel_version
from .macosx_libfile import calculate_macosx_platform_tag
from .metadata import pkginfo_to_metadata
from .util import log
from .vendored.packaging import tags
from .vendored.packaging import version as _packaging_version
from .wheelfile import WheelFile
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
"""
try:
# Better integration/compatibility with setuptools:
# in the case new fixes or PEPs are implemented in setuptools
# there is no need to backport them to the deprecated code base.
# This is useful in the case of old packages in the ecosystem
# that are still used but have low maintenance.
from setuptools.command.bdist_wheel import bdist_wheel
except ImportError:
# Only used in the case of old setuptools versions.
# If the user wants to get the latest fixes/PEPs,
# they are encouraged to address the deprecation warning.
from ._bdist_wheel import bdist_wheel as bdist_wheel
# normalize the version
return str(_packaging_version.Version(version))
except _packaging_version.InvalidVersion:
version = version.replace(" ", ".")
return re.sub("[^A-Za-z0-9.]+", "-", version)
setuptools_major_version = int(setuptools.__version__.split(".")[0])
PY_LIMITED_API_PATTERN = r"cp3\d"
def _is_32bit_interpreter():
return struct.calcsize("P") == 4
def python_tag():
return f"py{sys.version_info[0]}"
def get_platform(archive_root):
"""Return our platform name 'win32', 'linux_x86_64'"""
result = sysconfig.get_platform()
if result.startswith("macosx") and archive_root is not None:
result = calculate_macosx_platform_tag(archive_root, result)
elif _is_32bit_interpreter():
if result == "linux-x86_64":
# pip pull request #3497
result = "linux-i686"
elif result == "linux-aarch64":
# packaging pull request #234
# TODO armv8l, packaging pull request #690 => this did not land
# in pip/packaging yet
result = "linux-armv7l"
return result.replace("-", "_")
def get_flag(var, fallback, expected=True, warn=True):
"""Use a fallback value for determining SOABI flags if the needed config
var is unset or unavailable."""
val = sysconfig.get_config_var(var)
if val is None:
if warn:
warnings.warn(
f"Config variable '{var}' is unset, Python ABI tag may " "be incorrect",
RuntimeWarning,
stacklevel=2,
)
return fallback
return val == expected
def get_abi_tag():
"""Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy2)."""
soabi = sysconfig.get_config_var("SOABI")
impl = tags.interpreter_name()
if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"):
d = ""
m = ""
u = ""
if get_flag("Py_DEBUG", hasattr(sys, "gettotalrefcount"), warn=(impl == "cp")):
d = "d"
if get_flag(
"WITH_PYMALLOC",
impl == "cp",
warn=(impl == "cp" and sys.version_info < (3, 8)),
) and sys.version_info < (3, 8):
m = "m"
abi = f"{impl}{tags.interpreter_version()}{d}{m}{u}"
elif soabi and impl == "cp" and soabi.startswith("cpython"):
# non-Windows
abi = "cp" + soabi.split("-")[1]
elif soabi and impl == "cp" and soabi.startswith("cp"):
# Windows
abi = soabi.split("-")[0]
elif soabi and impl == "pp":
# we want something like pypy36-pp73
abi = "-".join(soabi.split("-")[:2])
abi = abi.replace(".", "_").replace("-", "_")
elif soabi and impl == "graalpy":
abi = "-".join(soabi.split("-")[:3])
abi = abi.replace(".", "_").replace("-", "_")
elif soabi:
abi = soabi.replace(".", "_").replace("-", "_")
else:
abi = None
return abi
def safer_name(name):
return safe_name(name).replace("-", "_")
def safer_version(version):
return safe_version(version).replace("-", "_")
def remove_readonly(func, path, excinfo):
remove_readonly_exc(func, path, excinfo[1])
def remove_readonly_exc(func, path, exc):
os.chmod(path, stat.S_IWRITE)
func(path)
class bdist_wheel(Command):
description = "create a wheel distribution"
supported_compressions = {
"stored": ZIP_STORED,
"deflated": ZIP_DEFLATED,
}
user_options = [
("bdist-dir=", "b", "temporary directory for creating the distribution"),
(
"plat-name=",
"p",
"platform name to embed in generated filenames "
"(default: %s)" % get_platform(None),
),
(
"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]",
),
("universal", None, "make a universal wheel" " (default: false)"),
(
"compression=",
None,
"zipfile compression (one of: {})" " (default: 'deflated')".format(
", ".join(supported_compressions)
),
),
(
"python-tag=",
None,
"Python implementation compatibility tag"
" (default: '%s')" % (python_tag()),
),
(
"build-number=",
None,
"Build number for this particular version. "
"As specified in PEP-0427, this must start with a digit. "
"[default: None]",
),
(
"py-limited-api=",
None,
"Python tag (cp32|cp33|cpNN) for abi3 wheel tag" " (default: false)",
),
]
boolean_options = ["keep-temp", "skip-build", "relative", "universal"]
def initialize_options(self):
self.bdist_dir = None
self.data_dir = None
self.plat_name = None
self.plat_tag = None
self.format = "zip"
self.keep_temp = False
self.dist_dir = None
self.egginfo_dir = None
self.root_is_pure = None
self.skip_build = None
self.relative = False
self.owner = None
self.group = None
self.universal = False
self.compression = "deflated"
self.python_tag = python_tag()
self.build_number = None
self.py_limited_api = False
self.plat_name_supplied = False
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, "wheel")
egg_info = self.distribution.get_command_obj("egg_info")
egg_info.ensure_finalized() # needed for correct `wheel_dist_name`
self.data_dir = self.wheel_dist_name + ".data"
self.plat_name_supplied = self.plat_name is not None
try:
self.compression = self.supported_compressions[self.compression]
except KeyError:
raise ValueError(f"Unsupported compression: {self.compression}") from None
need_options = ("dist_dir", "plat_name", "skip_build")
self.set_undefined_options("bdist", *zip(need_options, need_options))
self.root_is_pure = not (
self.distribution.has_ext_modules() or self.distribution.has_c_libraries()
)
if self.py_limited_api and not re.match(
PY_LIMITED_API_PATTERN, self.py_limited_api
):
raise ValueError("py-limited-api must match '%s'" % PY_LIMITED_API_PATTERN)
# Support legacy [wheel] section for setting universal
wheel = self.distribution.get_option_dict("wheel")
if "universal" in wheel:
# please don't define this in your global configs
log.warning(
"The [wheel] section is deprecated. Use [bdist_wheel] instead.",
)
val = wheel["universal"][1].strip()
if val.lower() in ("1", "true", "yes"):
self.universal = True
if self.build_number is not None and not self.build_number[:1].isdigit():
raise ValueError("Build tag (build-number) must start with a digit.")
@property
def wheel_dist_name(self):
"""Return distribution full name with - replaced with _"""
components = (
safer_name(self.distribution.get_name()),
safer_version(self.distribution.get_version()),
)
if self.build_number:
components += (self.build_number,)
return "-".join(components)
def get_tag(self):
# bdist sets self.plat_name if unset, we should only use it for purepy
# wheels if the user supplied it.
if self.plat_name_supplied:
plat_name = self.plat_name
elif self.root_is_pure:
plat_name = "any"
else:
# macosx contains system version in platform name so need special handle
if self.plat_name and not self.plat_name.startswith("macosx"):
plat_name = self.plat_name
else:
# on macosx always limit the platform name to comply with any
# c-extension modules in bdist_dir, since the user can specify
# a higher MACOSX_DEPLOYMENT_TARGET via tools like CMake
# on other platforms, and on macosx if there are no c-extension
# modules, use the default platform name.
plat_name = get_platform(self.bdist_dir)
if _is_32bit_interpreter():
if plat_name in ("linux-x86_64", "linux_x86_64"):
plat_name = "linux_i686"
if plat_name in ("linux-aarch64", "linux_aarch64"):
# TODO armv8l, packaging pull request #690 => this did not land
# in pip/packaging yet
plat_name = "linux_armv7l"
plat_name = (
plat_name.lower().replace("-", "_").replace(".", "_").replace(" ", "_")
)
if self.root_is_pure:
if self.universal:
impl = "py2.py3"
else:
impl = self.python_tag
tag = (impl, "none", plat_name)
else:
impl_name = tags.interpreter_name()
impl_ver = tags.interpreter_version()
impl = impl_name + impl_ver
# We don't work on CPython 3.1, 3.0.
if self.py_limited_api and (impl_name + impl_ver).startswith("cp3"):
impl = self.py_limited_api
abi_tag = "abi3"
else:
abi_tag = str(get_abi_tag()).lower()
tag = (impl, abi_tag, plat_name)
# issue gh-374: allow overriding plat_name
supported_tags = [
(t.interpreter, t.abi, plat_name) for t in tags.sys_tags()
]
assert (
tag in supported_tags
), f"would build wheel with unsupported tag {tag}"
return tag
def run(self):
build_scripts = self.reinitialize_command("build_scripts")
build_scripts.executable = "python"
build_scripts.force = True
build_ext = self.reinitialize_command("build_ext")
build_ext.inplace = False
if not self.skip_build:
self.run_command("build")
install = self.reinitialize_command("install", reinit_subcommands=True)
install.root = self.bdist_dir
install.compile = False
install.skip_build = self.skip_build
install.warn_dir = False
# A wheel without setuptools scripts is more cross-platform.
# Use the (undocumented) `no_ep` option to setuptools'
# install_scripts command to avoid creating entry point scripts.
install_scripts = self.reinitialize_command("install_scripts")
install_scripts.no_ep = True
# Use a custom scheme for the archive, because we have to decide
# at installation time which scheme to use.
for key in ("headers", "scripts", "data", "purelib", "platlib"):
setattr(install, "install_" + key, os.path.join(self.data_dir, key))
basedir_observed = ""
if os.name == "nt":
# win32 barfs if any of these are ''; could be '.'?
# (distutils.command.install:change_roots bug)
basedir_observed = os.path.normpath(os.path.join(self.data_dir, ".."))
self.install_libbase = self.install_lib = basedir_observed
setattr(
install,
"install_purelib" if self.root_is_pure else "install_platlib",
basedir_observed,
)
log.info(f"installing to {self.bdist_dir}")
self.run_command("install")
impl_tag, abi_tag, plat_tag = self.get_tag()
archive_basename = f"{self.wheel_dist_name}-{impl_tag}-{abi_tag}-{plat_tag}"
if not self.relative:
archive_root = self.bdist_dir
else:
archive_root = os.path.join(
self.bdist_dir, self._ensure_relative(install.install_base)
)
self.set_undefined_options("install_egg_info", ("target", "egginfo_dir"))
distinfo_dirname = (
f"{safer_name(self.distribution.get_name())}-"
f"{safer_version(self.distribution.get_version())}.dist-info"
)
distinfo_dir = os.path.join(self.bdist_dir, distinfo_dirname)
self.egg2dist(self.egginfo_dir, distinfo_dir)
self.write_wheelfile(distinfo_dir)
# Make the archive
if not os.path.exists(self.dist_dir):
os.makedirs(self.dist_dir)
wheel_path = os.path.join(self.dist_dir, archive_basename + ".whl")
with WheelFile(wheel_path, "w", self.compression) as wf:
wf.write_files(archive_root)
# Add to 'Distribution.dist_files' so that the "upload" command works
getattr(self.distribution, "dist_files", []).append(
(
"bdist_wheel",
"{}.{}".format(*sys.version_info[:2]), # like 3.7
wheel_path,
)
)
if not self.keep_temp:
log.info(f"removing {self.bdist_dir}")
if not self.dry_run:
if sys.version_info < (3, 12):
rmtree(self.bdist_dir, onerror=remove_readonly)
else:
rmtree(self.bdist_dir, onexc=remove_readonly_exc)
def write_wheelfile(
self, wheelfile_base, generator="bdist_wheel (" + wheel_version + ")"
):
from email.message import Message
msg = Message()
msg["Wheel-Version"] = "1.0" # of the spec
msg["Generator"] = generator
msg["Root-Is-Purelib"] = str(self.root_is_pure).lower()
if self.build_number is not None:
msg["Build"] = self.build_number
# Doesn't work for bdist_wininst
impl_tag, abi_tag, plat_tag = self.get_tag()
for impl in impl_tag.split("."):
for abi in abi_tag.split("."):
for plat in plat_tag.split("."):
msg["Tag"] = "-".join((impl, abi, plat))
wheelfile_path = os.path.join(wheelfile_base, "WHEEL")
log.info(f"creating {wheelfile_path}")
with open(wheelfile_path, "wb") as f:
BytesGenerator(f, maxheaderlen=0).flatten(msg)
def _ensure_relative(self, path):
# copied from dir_util, deleted
drive, path = os.path.splitdrive(path)
if path[0:1] == os.sep:
path = drive + path[1:]
return path
@property
def license_paths(self):
if setuptools_major_version >= 57:
# Setuptools has resolved any patterns to actual file names
return self.distribution.metadata.license_files or ()
files = set()
metadata = self.distribution.get_option_dict("metadata")
if setuptools_major_version >= 42:
# Setuptools recognizes the license_files option but does not do globbing
patterns = self.distribution.metadata.license_files
else:
# Prior to those, wheel is entirely responsible for handling license files
if "license_files" in metadata:
patterns = metadata["license_files"][1].split()
else:
patterns = ()
if "license_file" in metadata:
warnings.warn(
'The "license_file" option is deprecated. Use "license_files" instead.',
DeprecationWarning,
stacklevel=2,
)
files.add(metadata["license_file"][1])
if not files and not patterns and not isinstance(patterns, list):
patterns = ("LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*")
for pattern in patterns:
for path in iglob(pattern):
if path.endswith("~"):
log.debug(
f'ignoring license file "{path}" as it looks like a backup'
)
continue
if path not in files and os.path.isfile(path):
log.info(
f'adding license file "{path}" (matched pattern "{pattern}")'
)
files.add(path)
return files
def egg2dist(self, egginfo_path, distinfo_path):
"""Convert an .egg-info directory into a .dist-info directory"""
def adios(p):
"""Appropriately delete directory, file or link."""
if os.path.exists(p) and not os.path.islink(p) and os.path.isdir(p):
shutil.rmtree(p)
elif os.path.exists(p):
os.unlink(p)
adios(distinfo_path)
if not os.path.exists(egginfo_path):
# There is no egg-info. This is probably because the egg-info
# file/directory is not named matching the distribution name used
# to name the archive file. Check for this case and report
# accordingly.
import glob
pat = os.path.join(os.path.dirname(egginfo_path), "*.egg-info")
possible = glob.glob(pat)
err = f"Egg metadata expected at {egginfo_path} but not found"
if possible:
alt = os.path.basename(possible[0])
err += f" ({alt} found - possible misnamed archive file?)"
raise ValueError(err)
if os.path.isfile(egginfo_path):
# .egg-info is a single file
pkginfo_path = egginfo_path
pkg_info = pkginfo_to_metadata(egginfo_path, egginfo_path)
os.mkdir(distinfo_path)
else:
# .egg-info is a directory
pkginfo_path = os.path.join(egginfo_path, "PKG-INFO")
pkg_info = pkginfo_to_metadata(egginfo_path, pkginfo_path)
# ignore common egg metadata that is useless to wheel
shutil.copytree(
egginfo_path,
distinfo_path,
ignore=lambda x, y: {
"PKG-INFO",
"requires.txt",
"SOURCES.txt",
"not-zip-safe",
},
)
# delete dependency_links if it is only whitespace
dependency_links_path = os.path.join(distinfo_path, "dependency_links.txt")
with open(dependency_links_path, encoding="utf-8") as dependency_links_file:
dependency_links = dependency_links_file.read().strip()
if not dependency_links:
adios(dependency_links_path)
pkg_info_path = os.path.join(distinfo_path, "METADATA")
serialization_policy = EmailPolicy(
utf8=True,
mangle_from_=False,
max_line_length=0,
)
with open(pkg_info_path, "w", encoding="utf-8") as out:
Generator(out, policy=serialization_policy).flatten(pkg_info)
for license_path in self.license_paths:
filename = os.path.basename(license_path)
shutil.copy(license_path, os.path.join(distinfo_path, filename))
adios(egginfo_path)

@@ -17,3 +17,3 @@ """

def unpack_f(args: argparse.Namespace) -> None:
def unpack_f(args):
from .unpack import unpack

@@ -24,3 +24,3 @@

def pack_f(args: argparse.Namespace) -> None:
def pack_f(args):
from .pack import pack

@@ -31,3 +31,3 @@

def convert_f(args: argparse.Namespace) -> None:
def convert_f(args):
from .convert import convert

@@ -38,3 +38,3 @@

def tags_f(args: argparse.Namespace) -> None:
def tags_f(args):
from .tags import tags

@@ -58,6 +58,6 @@

def version_f(args: argparse.Namespace) -> None:
def version_f(args):
from .. import __version__
print(f"wheel {__version__}")
print("wheel %s" % __version__)

@@ -64,0 +64,0 @@

@@ -5,19 +5,17 @@ from __future__ import annotations

import re
from abc import ABCMeta, abstractmethod
from collections import defaultdict
from collections.abc import Iterator
from email.message import Message
from email.parser import Parser
from email.policy import EmailPolicy
import shutil
import tempfile
import zipfile
from glob import iglob
from pathlib import Path
from textwrap import dedent
from zipfile import ZipFile
from .. import __version__
from ..metadata import generate_requirements
from ..vendored.packaging.tags import parse_tag
from ..bdist_wheel import bdist_wheel
from ..wheelfile import WheelFile
from . import WheelError
egg_filename_re = re.compile(
try:
from setuptools import Distribution
except ImportError:
from distutils.dist import Distribution
egg_info_re = re.compile(
r"""

@@ -30,165 +28,81 @@ (?P<name>.+?)-(?P<ver>.+?)

)
egg_info_re = re.compile(
r"""
^(?P<name>.+?)-(?P<ver>.+?)
(-(?P<pyver>py\d\.\d+)
)?.egg-info/""",
re.VERBOSE,
)
wininst_re = re.compile(
r"\.(?P<platform>win32|win-amd64)(?:-(?P<pyver>py\d\.\d))?\.exe$"
)
pyd_re = re.compile(r"\.(?P<abi>[a-z0-9]+)-(?P<platform>win32|win_amd64)\.pyd$")
serialization_policy = EmailPolicy(
utf8=True,
mangle_from_=False,
max_line_length=0,
)
GENERATOR = f"wheel {__version__}"
def convert_requires(requires: str, metadata: Message) -> None:
extra: str | None = None
requirements: dict[str | None, list[str]] = defaultdict(list)
for line in requires.splitlines():
line = line.strip()
if not line:
continue
class _bdist_wheel_tag(bdist_wheel):
# allow the client to override the default generated wheel tag
# The default bdist_wheel implementation uses python and abi tags
# of the running python process. This is not suitable for
# generating/repackaging prebuild binaries.
if line.startswith("[") and line.endswith("]"):
extra = line[1:-1]
continue
full_tag_supplied = False
full_tag = None # None or a (pytag, soabitag, plattag) triple
requirements[extra].append(line)
for key, value in generate_requirements(requirements):
metadata.add_header(key, value)
def convert_pkg_info(pkginfo: str, metadata: Message):
parsed_message = Parser().parsestr(pkginfo)
for key, value in parsed_message.items():
key_lower = key.lower()
if value == "UNKNOWN":
continue
if key_lower == "description":
description_lines = value.splitlines()
value = "\n".join(
(
description_lines[0].lstrip(),
dedent("\n".join(description_lines[1:])),
"\n",
)
)
metadata.set_payload(value)
elif key_lower == "home-page":
metadata.add_header("Project-URL", f"Homepage, {value}")
elif key_lower == "download-url":
metadata.add_header("Project-URL", f"Download, {value}")
def get_tag(self):
if self.full_tag_supplied and self.full_tag is not None:
return self.full_tag
else:
metadata.add_header(key, value)
return bdist_wheel.get_tag(self)
metadata.replace_header("Metadata-Version", "2.4")
def egg2wheel(egg_path: str, dest_dir: str) -> None:
filename = os.path.basename(egg_path)
match = egg_info_re.match(filename)
if not match:
raise WheelError(f"Invalid egg file name: {filename}")
def normalize(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).lower().replace("-", "_")
egg_info = match.groupdict()
dir = tempfile.mkdtemp(suffix="_e2w")
if os.path.isfile(egg_path):
# assume we have a bdist_egg otherwise
with zipfile.ZipFile(egg_path) as egg:
egg.extractall(dir)
else:
# support buildout-style installed eggs directories
for pth in os.listdir(egg_path):
src = os.path.join(egg_path, pth)
if os.path.isfile(src):
shutil.copy2(src, dir)
else:
shutil.copytree(src, os.path.join(dir, pth))
pyver = egg_info["pyver"]
if pyver:
pyver = egg_info["pyver"] = pyver.replace(".", "")
class ConvertSource(metaclass=ABCMeta):
name: str
version: str
pyver: str = "py2.py3"
abi: str = "none"
platform: str = "any"
metadata: Message
arch = (egg_info["arch"] or "any").replace(".", "_").replace("-", "_")
@property
def dist_info_dir(self) -> str:
return f"{self.name}-{self.version}.dist-info"
# assume all binary eggs are for CPython
abi = "cp" + pyver[2:] if arch != "any" else "none"
@abstractmethod
def generate_contents(self) -> Iterator[tuple[str, bytes]]:
pass
root_is_purelib = egg_info["arch"] is None
if root_is_purelib:
bw = bdist_wheel(Distribution())
else:
bw = _bdist_wheel_tag(Distribution())
bw.root_is_pure = root_is_purelib
bw.python_tag = pyver
bw.plat_name_supplied = True
bw.plat_name = egg_info["arch"] or "any"
if not root_is_purelib:
bw.full_tag_supplied = True
bw.full_tag = (pyver, abi, arch)
class EggFileSource(ConvertSource):
def __init__(self, path: Path):
if not (match := egg_filename_re.match(path.name)):
raise ValueError(f"Invalid egg file name: {path.name}")
dist_info_dir = os.path.join(dir, "{name}-{ver}.dist-info".format(**egg_info))
bw.egg2dist(os.path.join(dir, "EGG-INFO"), dist_info_dir)
bw.write_wheelfile(dist_info_dir, generator="egg2wheel")
wheel_name = "{name}-{ver}-{pyver}-{}-{}.whl".format(abi, arch, **egg_info)
with WheelFile(os.path.join(dest_dir, wheel_name), "w") as wf:
wf.write_files(dir)
# Binary wheels are assumed to be for CPython
self.path = path
self.name = normalize(match.group("name"))
self.version = match.group("ver")
if pyver := match.group("pyver"):
self.pyver = pyver.replace(".", "")
if arch := match.group("arch"):
self.abi = self.pyver.replace("py", "cp")
self.platform = normalize(arch)
shutil.rmtree(dir)
self.metadata = Message()
def generate_contents(self) -> Iterator[tuple[str, bytes]]:
with ZipFile(self.path, "r") as zip_file:
for filename in sorted(zip_file.namelist()):
# Skip pure directory entries
if filename.endswith("/"):
continue
def parse_wininst_info(wininfo_name, egginfo_name):
"""Extract metadata from filenames.
# Handle files in the egg-info directory specially, selectively moving
# them to the dist-info directory while converting as needed
if filename.startswith("EGG-INFO/"):
if filename == "EGG-INFO/requires.txt":
requires = zip_file.read(filename).decode("utf-8")
convert_requires(requires, self.metadata)
elif filename == "EGG-INFO/PKG-INFO":
pkginfo = zip_file.read(filename).decode("utf-8")
convert_pkg_info(pkginfo, self.metadata)
elif filename == "EGG-INFO/entry_points.txt":
yield (
f"{self.dist_info_dir}/entry_points.txt",
zip_file.read(filename),
)
Extracts the 4 metadataitems needed (name, version, pyversion, arch) from
the installer filename and the name of the egg-info directory embedded in
the zipfile (if any).
continue
# For any other file, just pass it through
yield filename, zip_file.read(filename)
class EggDirectorySource(EggFileSource):
def generate_contents(self) -> Iterator[tuple[str, bytes]]:
for dirpath, _, filenames in os.walk(self.path):
for filename in sorted(filenames):
path = Path(dirpath, filename)
if path.parent.name == "EGG-INFO":
if path.name == "requires.txt":
requires = path.read_text("utf-8")
convert_requires(requires, self.metadata)
elif path.name == "PKG-INFO":
pkginfo = path.read_text("utf-8")
convert_pkg_info(pkginfo, self.metadata)
if name := self.metadata.get("Name"):
self.name = normalize(name)
if version := self.metadata.get("Version"):
self.version = version
elif path.name == "entry_points.txt":
yield (
f"{self.dist_info_dir}/entry_points.txt",
path.read_bytes(),
)
continue
# For any other file, just pass it through
yield str(path.relative_to(self.path)), path.read_bytes()
class WininstFileSource(ConvertSource):
"""
Handles distributions created with ``bdist_wininst``.
The egginfo filename has the format::

@@ -220,116 +134,143 @@

def __init__(self, path: Path):
self.path = path
self.metadata = Message()
egginfo = None
if egginfo_name:
egginfo = egg_info_re.search(egginfo_name)
if not egginfo:
raise ValueError(f"Egg info filename {egginfo_name} is not valid")
# Determine the initial architecture and Python version from the file name
# (if possible)
if match := wininst_re.search(path.name):
self.platform = normalize(match.group("platform"))
if pyver := match.group("pyver"):
self.pyver = pyver.replace(".", "")
# Parse the wininst filename
# 1. Distribution name (up to the first '-')
w_name, sep, rest = wininfo_name.partition("-")
if not sep:
raise ValueError(f"Installer filename {wininfo_name} is not valid")
# Look for an .egg-info directory and any .pyd files for more precise info
egg_info_found = pyd_found = False
with ZipFile(self.path) as zip_file:
for filename in zip_file.namelist():
prefix, filename = filename.split("/", 1)
if not egg_info_found and (match := egg_info_re.match(filename)):
egg_info_found = True
self.name = normalize(match.group("name"))
self.version = match.group("ver")
if pyver := match.group("pyver"):
self.pyver = pyver.replace(".", "")
elif not pyd_found and (match := pyd_re.search(filename)):
pyd_found = True
self.abi = match.group("abi")
self.platform = match.group("platform")
# Strip '.exe'
rest = rest[:-4]
# 2. Python version (from the last '-', must start with 'py')
rest2, sep, w_pyver = rest.rpartition("-")
if sep and w_pyver.startswith("py"):
rest = rest2
w_pyver = w_pyver.replace(".", "")
else:
# Not version specific - use py2.py3. While it is possible that
# pure-Python code is not compatible with both Python 2 and 3, there
# is no way of knowing from the wininst format, so we assume the best
# here (the user can always manually rename the wheel to be more
# restrictive if needed).
w_pyver = "py2.py3"
# 3. Version and architecture
w_ver, sep, w_arch = rest.rpartition(".")
if not sep:
raise ValueError(f"Installer filename {wininfo_name} is not valid")
if egg_info_found and pyd_found:
break
if egginfo:
w_name = egginfo.group("name")
w_ver = egginfo.group("ver")
def generate_contents(self) -> Iterator[tuple[str, bytes]]:
dist_info_dir = f"{self.name}-{self.version}.dist-info"
data_dir = f"{self.name}-{self.version}.data"
with ZipFile(self.path, "r") as zip_file:
for filename in sorted(zip_file.namelist()):
# Skip pure directory entries
if filename.endswith("/"):
continue
return {"name": w_name, "ver": w_ver, "arch": w_arch, "pyver": w_pyver}
# Handle files in the egg-info directory specially, selectively moving
# them to the dist-info directory while converting as needed
prefix, target_filename = filename.split("/", 1)
if egg_info_re.search(target_filename):
basename = target_filename.rsplit("/", 1)[-1]
if basename == "requires.txt":
requires = zip_file.read(filename).decode("utf-8")
convert_requires(requires, self.metadata)
elif basename == "PKG-INFO":
pkginfo = zip_file.read(filename).decode("utf-8")
convert_pkg_info(pkginfo, self.metadata)
elif basename == "entry_points.txt":
yield (
f"{dist_info_dir}/entry_points.txt",
zip_file.read(filename),
)
continue
elif prefix == "SCRIPTS":
target_filename = f"{data_dir}/scripts/{target_filename}"
def wininst2wheel(path, dest_dir):
with zipfile.ZipFile(path) as bdw:
# Search for egg-info in the archive
egginfo_name = None
for filename in bdw.namelist():
if ".egg-info" in filename:
egginfo_name = filename
break
# For any other file, just pass it through
yield target_filename, zip_file.read(filename)
info = parse_wininst_info(os.path.basename(path), egginfo_name)
root_is_purelib = True
for zipinfo in bdw.infolist():
if zipinfo.filename.startswith("PLATLIB"):
root_is_purelib = False
break
if root_is_purelib:
paths = {"purelib": ""}
else:
paths = {"platlib": ""}
def convert(files: list[str], dest_dir: str, verbose: bool) -> None:
for pat in files:
for archive in iglob(pat):
path = Path(archive)
if path.suffix == ".egg":
if path.is_dir():
source: ConvertSource = EggDirectorySource(path)
else:
source = EggFileSource(path)
else:
source = WininstFileSource(path)
dist_info = "{name}-{ver}".format(**info)
datadir = "%s.data/" % dist_info
if verbose:
print(f"{archive}...", flush=True, end="")
# rewrite paths to trick ZipFile into extracting an egg
# XXX grab wininst .ini - between .exe, padding, and first zip file.
members = []
egginfo_name = ""
for zipinfo in bdw.infolist():
key, basename = zipinfo.filename.split("/", 1)
key = key.lower()
basepath = paths.get(key, None)
if basepath is None:
basepath = datadir + key.lower() + "/"
oldname = zipinfo.filename
newname = basepath + basename
zipinfo.filename = newname
del bdw.NameToInfo[oldname]
bdw.NameToInfo[newname] = zipinfo
# Collect member names, but omit '' (from an entry like "PLATLIB/"
if newname:
members.append(newname)
# Remember egg-info name for the egg2dist call below
if not egginfo_name:
if newname.endswith(".egg-info"):
egginfo_name = newname
elif ".egg-info/" in newname:
egginfo_name, sep, _ = newname.rpartition("/")
dir = tempfile.mkdtemp(suffix="_b2w")
bdw.extractall(dir, members)
dest_path = Path(dest_dir) / (
f"{source.name}-{source.version}-{source.pyver}-{source.abi}"
f"-{source.platform}.whl"
)
with WheelFile(dest_path, "w") as wheelfile:
for name_or_zinfo, contents in source.generate_contents():
wheelfile.writestr(name_or_zinfo, contents)
# egg2wheel
abi = "none"
pyver = info["pyver"]
arch = (info["arch"] or "any").replace(".", "_").replace("-", "_")
# Wininst installers always have arch even if they are not
# architecture-specific (because the format itself is).
# So, assume the content is architecture-neutral if root is purelib.
if root_is_purelib:
arch = "any"
# If the installer is architecture-specific, it's almost certainly also
# CPython-specific.
if arch != "any":
pyver = pyver.replace("py", "cp")
wheel_name = "-".join((dist_info, pyver, abi, arch))
if root_is_purelib:
bw = bdist_wheel(Distribution())
else:
bw = _bdist_wheel_tag(Distribution())
# Write the METADATA file
wheelfile.writestr(
f"{source.dist_info_dir}/METADATA",
source.metadata.as_string(policy=serialization_policy).encode(
"utf-8"
),
)
bw.root_is_pure = root_is_purelib
bw.python_tag = pyver
bw.plat_name_supplied = True
bw.plat_name = info["arch"] or "any"
# Write the WHEEL file
wheel_message = Message()
wheel_message.add_header("Wheel-Version", "1.0")
wheel_message.add_header("Generator", GENERATOR)
wheel_message.add_header(
"Root-Is-Purelib", str(source.platform == "any").lower()
)
tags = parse_tag(f"{source.pyver}-{source.abi}-{source.platform}")
for tag in sorted(tags, key=lambda tag: tag.interpreter):
wheel_message.add_header("Tag", str(tag))
if not root_is_purelib:
bw.full_tag_supplied = True
bw.full_tag = (pyver, abi, arch)
wheelfile.writestr(
f"{source.dist_info_dir}/WHEEL",
wheel_message.as_string(policy=serialization_policy).encode(
"utf-8"
),
)
dist_info_dir = os.path.join(dir, "%s.dist-info" % dist_info)
bw.egg2dist(os.path.join(dir, egginfo_name), dist_info_dir)
bw.write_wheelfile(dist_info_dir, generator="wininst2wheel")
wheel_path = os.path.join(dest_dir, wheel_name)
with WheelFile(wheel_path, "w") as wf:
wf.write_files(dir)
shutil.rmtree(dir)
def convert(files, dest_dir, verbose):
for pat in files:
for installer in iglob(pat):
if os.path.splitext(installer)[1] == ".egg":
conv = egg2wheel
else:
conv = wininst2wheel
if verbose:
print(f"{installer}... ", flush=True)
conv(installer, dest_dir)
if verbose:
print("OK")

@@ -46,10 +46,3 @@ """

import sys
from io import BufferedIOBase
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Union
StrPath = Union[str, os.PathLike[str]]
"""here the needed const and struct from mach-o header files"""

@@ -249,3 +242,3 @@

def swap32(x: int) -> int:
def swap32(x):
return (

@@ -259,6 +252,3 @@ ((x << 24) & 0xFF000000)

def get_base_class_and_magic_number(
lib_file: BufferedIOBase,
seek: int | None = None,
) -> tuple[type[ctypes.Structure], int]:
def get_base_class_and_magic_number(lib_file, seek=None):
if seek is None:

@@ -287,7 +277,7 @@ seek = lib_file.tell()

def read_data(struct_class: type[ctypes.Structure], lib_file: BufferedIOBase):
def read_data(struct_class, lib_file):
return struct_class.from_buffer_copy(lib_file.read(ctypes.sizeof(struct_class)))
def extract_macosx_min_system_version(path_to_lib: str):
def extract_macosx_min_system_version(path_to_lib):
with open(path_to_lib, "rb") as lib_file:

@@ -318,3 +308,3 @@ BaseClass, magic_number = get_base_class_and_magic_number(lib_file, 0)

versions_list: list[tuple[int, int, int]] = []
versions_list = []
for el in fat_arch_list:

@@ -351,6 +341,3 @@ try:

def read_mach_header(
lib_file: BufferedIOBase,
seek: int | None = None,
) -> tuple[int, int, int] | None:
def read_mach_header(lib_file, seek=None):
"""

@@ -402,3 +389,3 @@ This function parses a Mach-O header and extracts

def parse_version(version: int) -> tuple[int, int, int]:
def parse_version(version):
x = (version & 0xFFFF0000) >> 16

@@ -410,3 +397,3 @@ y = (version & 0x0000FF00) >> 8

def calculate_macosx_platform_tag(archive_root: StrPath, platform_tag: str) -> str:
def calculate_macosx_platform_tag(archive_root, platform_tag):
"""

@@ -444,3 +431,3 @@ Calculate proper macosx platform tag basing on files which are included to wheel

start_version = base_version
versions_dict: dict[str, tuple[int, int]] = {}
versions_dict = {}
for dirpath, _dirnames, filenames in os.walk(archive_root):

@@ -447,0 +434,0 @@ for filename in filenames:

@@ -14,3 +14,3 @@ """

from email.parser import Parser
from typing import Generator, Iterable, Iterator, Literal
from typing import Iterator

@@ -20,3 +20,3 @@ from .vendored.packaging.requirements import Requirement

def _nonblank(str: str) -> bool | Literal[""]:
def _nonblank(str):
return str and not str.startswith("#")

@@ -26,3 +26,3 @@

@functools.singledispatch
def yield_lines(iterable: Iterable[str]) -> Iterator[str]:
def yield_lines(iterable):
r"""

@@ -45,9 +45,7 @@ Yield valid lines of a string or iterable.

@yield_lines.register(str)
def _(text: str) -> Iterator[str]:
def _(text):
return filter(_nonblank, map(str.strip, text.splitlines()))
def split_sections(
s: str | Iterator[str],
) -> Generator[tuple[str | None, list[str]], None, None]:
def split_sections(s):
"""Split a string or iterable thereof into (section, content) pairs

@@ -60,3 +58,3 @@ Each ``section`` is a stripped version of the section header ("[section]")

section = None
content: list[str] = []
content = []
for line in yield_lines(s):

@@ -78,3 +76,3 @@ if line.startswith("["):

def safe_extra(extra: str) -> str:
def safe_extra(extra):
"""Convert an arbitrary string to a standard 'extra' name

@@ -87,3 +85,3 @@ Any runs of non-alphanumeric characters are replaced with a single '_',

def safe_name(name: str) -> str:
def safe_name(name):
"""Convert an arbitrary string to a standard distribution name

@@ -97,6 +95,6 @@ Any runs of non-alphanumeric/. characters are replaced with a single '-'.

"""Return the version specifier for a requirement in PEP 345/566 fashion."""
if requirement.url:
if getattr(requirement, "url", None):
return " @ " + requirement.url
requires_dist: list[str] = []
requires_dist = []
for spec in requirement.specifier:

@@ -124,3 +122,3 @@ requires_dist.append(spec.operator + spec.version)

def generate_requirements(
extras_require: dict[str | None, list[str]],
extras_require: dict[str, list[str]],
) -> Iterator[tuple[str, str]]:

@@ -145,3 +143,3 @@ """

condition = "(" + condition + ") and "
condition += f"extra == '{extra}'"
condition += "extra == '%s'" % extra

@@ -152,4 +150,3 @@ if condition:

for new_req in convert_requirements(depends):
canonical_req = str(Requirement(new_req + condition))
yield "Requires-Dist", canonical_req
yield "Requires-Dist", new_req + condition

@@ -156,0 +153,0 @@

@@ -8,3 +8,12 @@ from __future__ import annotations

# ensure Python logging is configured
try:
__import__("setuptools.logging")
except ImportError:
# setuptools < ??
from . import _setuptools_logging
_setuptools_logging.configure()
def urlsafe_b64encode(data: bytes) -> bytes:

@@ -11,0 +20,0 @@ """urlsafe_b64encode without padding"""

@@ -10,3 +10,2 @@ from __future__ import annotations

from io import StringIO, TextIOWrapper
from typing import IO, TYPE_CHECKING, Literal
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo

@@ -17,12 +16,2 @@

if TYPE_CHECKING:
from typing import Protocol, Sized, Union
from typing_extensions import Buffer
StrPath = Union[str, os.PathLike[str]]
class SizedBuffer(Sized, Buffer, Protocol): ...
# Non-greedy matching of an optional build number may be too clever (more

@@ -38,3 +27,3 @@ # invalid wheel filenames will match). Separate regex for .dist-info?

def get_zipinfo_datetime(timestamp: float | None = None):
def get_zipinfo_datetime(timestamp=None):
# Some applications need reproducible .whl files, but they can't do this without

@@ -54,8 +43,3 @@ # forcing the timestamp of the individual ZipInfo objects. See issue #143.

def __init__(
self,
file: StrPath,
mode: Literal["r", "w", "x", "a"] = "r",
compression: int = ZIP_DEFLATED,
):
def __init__(self, file, mode="r", compression=ZIP_DEFLATED):
basename = os.path.basename(file)

@@ -72,3 +56,3 @@ self.parsed_filename = WHEEL_INFO_RE.match(basename)

self.record_path = self.dist_info_path + "/RECORD"
self._file_hashes: dict[str, tuple[None, None] | tuple[int, bytes]] = {}
self._file_hashes = {}
self._file_sizes = {}

@@ -114,9 +98,4 @@ if mode == "r":

def open(
self,
name_or_info: str | ZipInfo,
mode: Literal["r", "w"] = "r",
pwd: bytes | None = None,
) -> IO[bytes]:
def _update_crc(newdata: bytes) -> None:
def open(self, name_or_info, mode="r", pwd=None):
def _update_crc(newdata):
eof = ef._eof

@@ -149,5 +128,5 @@ update_crc_orig(newdata)

def write_files(self, base_dir: str):
def write_files(self, base_dir):
log.info(f"creating '{self.filename}' and adding '{base_dir}' to it")
deferred: list[tuple[str, str]] = []
deferred = []
for root, dirnames, filenames in os.walk(base_dir):

@@ -172,8 +151,3 @@ # Sort the directory names so that `os.walk` will walk them in a

def write(
self,
filename: str,
arcname: str | None = None,
compress_type: int | None = None,
) -> None:
def write(self, filename, arcname=None, compress_type=None):
with open(filename, "rb") as f:

@@ -190,8 +164,3 @@ st = os.fstat(f.fileno())

def writestr(
self,
zinfo_or_arcname: str | ZipInfo,
data: SizedBuffer | str,
compress_type: int | None = None,
):
def writestr(self, zinfo_or_arcname, data, compress_type=None):
if isinstance(zinfo_or_arcname, str):

@@ -198,0 +167,0 @@ zinfo_or_arcname = ZipInfo(

@@ -34,5 +34,3 @@ """Utilities for extracting common archive formats"""

def unpack_archive(
filename, extract_dir, progress_filter=default_filter, drivers=None
) -> None:
def unpack_archive(filename, extract_dir, progress_filter=default_filter, drivers=None):
"""Unpack `filename` to `extract_dir`, or raise ``UnrecognizedFormat``

@@ -66,6 +64,6 @@

else:
raise UnrecognizedFormat(f"Not a recognized archive type: {filename}")
raise UnrecognizedFormat("Not a recognized archive type: %s" % filename)
def unpack_directory(filename, extract_dir, progress_filter=default_filter) -> None:
def unpack_directory(filename, extract_dir, progress_filter=default_filter):
""" "Unpack" a directory, using the same interface as for archives

@@ -76,3 +74,3 @@

if not os.path.isdir(filename):
raise UnrecognizedFormat(f"{filename} is not a directory")
raise UnrecognizedFormat("%s is not a directory" % filename)

@@ -98,3 +96,3 @@ paths = {

def unpack_zipfile(filename, extract_dir, progress_filter=default_filter) -> None:
def unpack_zipfile(filename, extract_dir, progress_filter=default_filter):
"""Unpack zip `filename` to `extract_dir`

@@ -108,3 +106,3 @@

if not zipfile.is_zipfile(filename):
raise UnrecognizedFormat(f"{filename} is not a zip file")
raise UnrecognizedFormat("%s is not a zip file" % (filename,))

@@ -195,3 +193,3 @@ with zipfile.ZipFile(filename) as z:

def unpack_tarfile(filename, extract_dir, progress_filter=default_filter) -> bool:
def unpack_tarfile(filename, extract_dir, progress_filter=default_filter):
"""Unpack tar/tar.gz/tar.bz2 `filename` to `extract_dir`

@@ -207,3 +205,3 @@

raise UnrecognizedFormat(
f"{filename} is not a compressed or uncompressed tar file"
"%s is not a compressed or uncompressed tar file" % (filename,)
) from e

@@ -210,0 +208,0 @@

@@ -40,5 +40,4 @@ """A PEP 517 interface to setuptools

import warnings
from collections.abc import Iterable, Iterator, Mapping
from pathlib import Path
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING, Iterable, Iterator, List, Mapping, Union

@@ -71,5 +70,8 @@ import setuptools

SETUPTOOLS_ENABLE_FEATURES = os.getenv("SETUPTOOLS_ENABLE_FEATURES", "").lower()
LEGACY_EDITABLE = "legacy-editable" in SETUPTOOLS_ENABLE_FEATURES.replace("_", "-")
class SetupRequirementsError(BaseException):
def __init__(self, specifiers) -> None:
def __init__(self, specifiers):
self.specifiers = specifiers

@@ -93,7 +95,7 @@

orig = distutils.core.Distribution
distutils.core.Distribution = cls # type: ignore[misc] # monkeypatching
distutils.core.Distribution = cls
try:
yield
finally:
distutils.core.Distribution = orig # type: ignore[misc] # monkeypatching
distutils.core.Distribution = orig

@@ -150,3 +152,3 @@

_ConfigSettings: TypeAlias = Union[Mapping[str, Union[str, list[str], None]], None]
_ConfigSettings: TypeAlias = Union[Mapping[str, Union[str, List[str], None]], None]
"""

@@ -461,26 +463,33 @@ Currently the user can run::

def build_editable(
self,
wheel_directory: StrPath,
config_settings: _ConfigSettings = None,
metadata_directory: StrPath | None = None,
):
# XXX can or should we hide our editable_wheel command normally?
info_dir = self._get_dist_info_dir(metadata_directory)
opts = ["--dist-info-dir", info_dir] if info_dir else []
cmd = ["editable_wheel", *opts, *self._editable_args(config_settings)]
with suppress_known_deprecation():
return self._build_with_temp_dir(
cmd, ".whl", wheel_directory, config_settings
)
if not LEGACY_EDITABLE:
# PEP660 hooks:
# build_editable
# get_requires_for_build_editable
# prepare_metadata_for_build_editable
def build_editable(
self,
wheel_directory: StrPath,
config_settings: _ConfigSettings = None,
metadata_directory: StrPath | None = None,
):
# XXX can or should we hide our editable_wheel command normally?
info_dir = self._get_dist_info_dir(metadata_directory)
opts = ["--dist-info-dir", info_dir] if info_dir else []
cmd = ["editable_wheel", *opts, *self._editable_args(config_settings)]
with suppress_known_deprecation():
return self._build_with_temp_dir(
cmd, ".whl", wheel_directory, config_settings
)
def get_requires_for_build_editable(self, config_settings: _ConfigSettings = None):
return self.get_requires_for_build_wheel(config_settings)
def get_requires_for_build_editable(
self, config_settings: _ConfigSettings = None
):
return self.get_requires_for_build_wheel(config_settings)
def prepare_metadata_for_build_editable(
self, metadata_directory: StrPath, config_settings: _ConfigSettings = None
):
return self.prepare_metadata_for_build_wheel(
metadata_directory, config_settings
)
def prepare_metadata_for_build_editable(
self, metadata_directory: StrPath, config_settings: _ConfigSettings = None
):
return self.prepare_metadata_for_build_wheel(
metadata_directory, config_settings
)

@@ -547,8 +556,10 @@

build_sdist = _BACKEND.build_sdist
get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable
prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_editable
build_editable = _BACKEND.build_editable
if not LEGACY_EDITABLE:
get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable
prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_editable
build_editable = _BACKEND.build_editable
# The legacy backend
__legacy__ = _BuildMetaLegacyBackend()

@@ -14,5 +14,4 @@ """Helper code used to generate ``requires.txt`` files in the egg-info directory.

from collections import defaultdict
from collections.abc import Mapping
from itertools import filterfalse
from typing import TypeVar
from typing import Dict, Mapping, TypeVar

@@ -27,3 +26,3 @@ from jaraco.text import yield_lines

_T = TypeVar("_T")
_Ordered = dict[_T, None]
_Ordered = Dict[_T, None]

@@ -43,3 +42,3 @@

extras_require: Mapping[str, _StrOrIter],
) -> defaultdict[str, _Ordered[Requirement]]:
) -> Mapping[str, _Ordered[Requirement]]:
"""

@@ -50,3 +49,3 @@ Convert requirements in `extras_require` of the form

"""
output = defaultdict[str, _Ordered[Requirement]](dict)
output: Mapping[str, _Ordered[Requirement]] = defaultdict(dict)
for section, v in extras_require.items():

@@ -53,0 +52,0 @@ # Do not strip empty sections.

@@ -33,3 +33,3 @@ from setuptools.command.setopt import config_file, edit_config, option_base

def finalize_options(self) -> None:
def finalize_options(self):
option_base.finalize_options(self)

@@ -41,3 +41,3 @@ if self.remove and len(self.args) != 1:

def run(self) -> None:
def run(self):
aliases = self.distribution.get_option_dict('aliases')

@@ -60,3 +60,3 @@

else:
print(f"No alias definition found for {alias!r}")
print("No alias definition found for %r" % alias)
return

@@ -79,3 +79,3 @@ else:

else:
source = f'--filename={source!r}'
source = '--filename=%r' % source
return source + name + ' ' + command

@@ -12,3 +12,3 @@ """setuptools.command.bdist_egg

import textwrap
from sysconfig import get_path, get_platform, get_python_version
from sysconfig import get_path, get_python_version
from types import CodeType

@@ -20,3 +20,3 @@ from typing import TYPE_CHECKING, Literal

from .._path import StrPathT, ensure_directory
from .._path import ensure_directory

@@ -55,3 +55,3 @@ from distutils import log

def write_stub(resource, pyfile) -> None:
def write_stub(resource, pyfile):
_stub_template = textwrap.dedent(

@@ -61,8 +61,8 @@ """

global __bootstrap__, __loader__, __file__
import sys, importlib.resources as irs, importlib.util
with irs.as_file(irs.files(__name__).joinpath(%r)) as __file__:
__loader__ = None; del __bootstrap__, __loader__
spec = importlib.util.spec_from_file_location(__name__,__file__)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
import sys, pkg_resources, importlib.util
__file__ = pkg_resources.resource_filename(__name__, %r)
__loader__ = None; del __bootstrap__, __loader__
spec = importlib.util.spec_from_file_location(__name__,__file__)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
__bootstrap__()

@@ -76,3 +76,3 @@ """

class bdist_egg(Command):
description = 'create an "egg" distribution'
description = "create an \"egg\" distribution"

@@ -85,3 +85,3 @@ user_options = [

"platform name to embed in generated filenames "
"(by default uses `sysconfig.get_platform()`)",
"(by default uses `pkg_resources.get_build_platform()`)",
),

@@ -110,3 +110,3 @@ ('exclude-source-files', None, "remove all .py files from the generated egg"),

def finalize_options(self) -> None:
def finalize_options(self):
ei_cmd = self.ei_cmd = self.get_finalized_command("egg_info")

@@ -120,4 +120,6 @@ self.egg_info = ei_cmd.egg_info

if self.plat_name is None:
self.plat_name = get_platform()
from pkg_resources import get_build_platform
self.plat_name = get_build_platform()
self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))

@@ -134,3 +136,3 @@

def do_install_data(self) -> None:
def do_install_data(self):
# Hack for packages that install data to install's --install-lib

@@ -191,3 +193,3 @@ self.get_finalized_command('install').install_lib = self.bdist_dir

for p, ext_name in enumerate(ext_outputs):
filename, _ext = os.path.splitext(ext_name)
filename, ext = os.path.splitext(ext_name)
pyfile = os.path.join(self.bdist_dir, strip_module(filename) + '.py')

@@ -274,3 +276,3 @@ self.stubs.append(pyfile)

path_new = os.path.join(base, os.pardir, m.group('name') + '.pyc')
log.info(f"Renaming file from [{path_old}] to [{path_new}]")
log.info("Renaming file from [%s] to [%s]" % (path_old, path_new))
try:

@@ -289,6 +291,6 @@ os.remove(path_new)

def gen_header(self) -> Literal["w"]:
def gen_header(self):
return 'w'
def copy_metadata_to(self, target_dir) -> None:
def copy_metadata_to(self, target_dir):
"Copy metadata (egg info) to the target_dir"

@@ -335,3 +337,3 @@ # normalize the path (so that a forward-slash in egg_info will

NATIVE_EXTENSIONS: dict[str, None] = dict.fromkeys('.dll .so .dylib .pyd'.split())
NATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split())

@@ -367,3 +369,3 @@

def write_safety_flag(egg_dir, safe) -> None:
def write_safety_flag(egg_dir, safe):
# Write or remove zip safety flag file(s)

@@ -436,3 +438,3 @@ for flag, fn in safety_flags.items():

def can_scan() -> bool:
def can_scan():
if not sys.platform.startswith('java') and sys.platform != 'cli':

@@ -456,3 +458,3 @@ # CPython, PyPy, etc.

def make_zipfile(
zip_filename: StrPathT,
zip_filename,
base_dir,

@@ -463,3 +465,3 @@ verbose: bool = False,

mode: _ZipFileMode = 'w',
) -> StrPathT:
):
"""Create a zip file from all the files under 'base_dir'. The output

@@ -473,3 +475,3 @@ zip file will be named 'base_dir' + ".zip". Uses either the "zipfile"

mkpath(os.path.dirname(zip_filename), dry_run=dry_run) # type: ignore[arg-type] # python/mypy#18075
mkpath(os.path.dirname(zip_filename), dry_run=dry_run)
log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir)

@@ -476,0 +478,0 @@

@@ -18,3 +18,3 @@ from ..dist import Distribution

def run(self) -> None:
def run(self):
SetuptoolsDeprecationWarning.emit(

@@ -21,0 +21,0 @@ "Deprecated command",

@@ -12,2 +12,3 @@ """

import shutil
import stat
import struct

@@ -17,13 +18,14 @@ import sys

import warnings
from collections.abc import Iterable, Sequence
from email.generator import BytesGenerator
from email.generator import BytesGenerator, Generator
from email.policy import EmailPolicy
from glob import iglob
from typing import Literal, cast
from shutil import rmtree
from typing import TYPE_CHECKING, Callable, Iterable, Literal, Sequence, cast
from zipfile import ZIP_DEFLATED, ZIP_STORED
from packaging import tags, version as _packaging_version
from wheel.metadata import pkginfo_to_metadata
from wheel.wheelfile import WheelFile
from .. import Command, __version__, _shutil
from .._core_metadata import _safe_license_file
from .. import Command, __version__
from .._normalization import safer_name

@@ -35,3 +37,6 @@ from ..warnings import SetuptoolsDeprecationWarning

if TYPE_CHECKING:
from _typeshed import ExcInfo
def safe_version(version: str) -> str:

@@ -59,3 +64,3 @@ """

def python_tag() -> str:
return f"py{sys.version_info.major}"
return f"py{sys.version_info[0]}"

@@ -106,2 +111,3 @@

d = ""
m = ""
u = ""

@@ -111,3 +117,10 @@ if get_flag("Py_DEBUG", hasattr(sys, "gettotalrefcount"), warn=(impl == "cp")):

abi = f"{impl}{tags.interpreter_version()}{d}{u}"
if get_flag(
"WITH_PYMALLOC",
impl == "cp",
warn=(impl == "cp" and sys.version_info < (3, 8)),
) and sys.version_info < (3, 8):
m = "m"
abi = f"{impl}{tags.interpreter_version()}{d}{m}{u}"
elif soabi and impl == "cp" and soabi.startswith("cpython"):

@@ -141,2 +154,17 @@ # non-Windows

def remove_readonly(
func: Callable[..., object],
path: str,
excinfo: ExcInfo,
) -> None:
remove_readonly_exc(func, path, excinfo[1])
def remove_readonly_exc(
func: Callable[..., object], path: str, exc: BaseException
) -> None:
os.chmod(path, stat.S_IWRITE)
func(path)
class bdist_wheel(Command):

@@ -185,3 +213,5 @@ description = "create a wheel distribution"

None,
f"zipfile compression (one of: {', '.join(supported_compressions)}) [default: 'deflated']",
"zipfile compression (one of: {}) [default: 'deflated']".format(
", ".join(supported_compressions)
),
),

@@ -218,3 +248,3 @@ (

self.bdist_dir: str | None = None
self.data_dir = ""
self.data_dir: str | None = None
self.plat_name: str | None = None

@@ -297,3 +327,3 @@ self.plat_tag: str | None = None

"`Py_LIMITED_API` is currently incompatible with "
"`Py_GIL_DISABLED`. "
f"`Py_GIL_DISABLED` ({sys.abiflags=!r}). "
"See https://github.com/python/cpython/issues/111506."

@@ -438,3 +468,3 @@ )

# copied into the wheel.
_shutil.rmtree(self.egginfo_dir)
shutil.rmtree(self.egginfo_dir)
else:

@@ -457,3 +487,3 @@ # Convert the generated egg-info into dist-info.

"bdist_wheel",
f"{sys.version_info.major}.{sys.version_info.minor}",
"{}.{}".format(*sys.version_info[:2]), # like 3.7
wheel_path,

@@ -465,3 +495,6 @@ ))

if not self.dry_run:
_shutil.rmtree(self.bdist_dir)
if sys.version_info < (3, 12):
rmtree(self.bdist_dir, onerror=remove_readonly)
else:
rmtree(self.bdist_dir, onexc=remove_readonly_exc)

@@ -505,3 +538,3 @@ def write_wheelfile(

files = set[str]()
files: set[str] = set()
metadata = self.distribution.get_option_dict("metadata")

@@ -551,3 +584,3 @@ if setuptools_major_version >= 42:

if os.path.exists(p) and not os.path.islink(p) and os.path.isdir(p):
_shutil.rmtree(p)
shutil.rmtree(p)
elif os.path.exists(p):

@@ -574,33 +607,42 @@ os.unlink(p)

# .egg-info is a directory
pkginfo_path = os.path.join(egginfo_path, "PKG-INFO")
if os.path.isfile(egginfo_path):
# .egg-info is a single file
pkg_info = pkginfo_to_metadata(egginfo_path, egginfo_path)
os.mkdir(distinfo_path)
else:
# .egg-info is a directory
pkginfo_path = os.path.join(egginfo_path, "PKG-INFO")
pkg_info = pkginfo_to_metadata(egginfo_path, pkginfo_path)
# ignore common egg metadata that is useless to wheel
shutil.copytree(
egginfo_path,
distinfo_path,
ignore=lambda x, y: {
"PKG-INFO",
"requires.txt",
"SOURCES.txt",
"not-zip-safe",
},
)
# ignore common egg metadata that is useless to wheel
shutil.copytree(
egginfo_path,
distinfo_path,
ignore=lambda x, y: {
"PKG-INFO",
"requires.txt",
"SOURCES.txt",
"not-zip-safe",
},
)
# delete dependency_links if it is only whitespace
dependency_links_path = os.path.join(distinfo_path, "dependency_links.txt")
with open(dependency_links_path, encoding="utf-8") as dependency_links_file:
dependency_links = dependency_links_file.read().strip()
if not dependency_links:
adios(dependency_links_path)
# delete dependency_links if it is only whitespace
dependency_links_path = os.path.join(distinfo_path, "dependency_links.txt")
with open(dependency_links_path, encoding="utf-8") as dependency_links_file:
dependency_links = dependency_links_file.read().strip()
if not dependency_links:
adios(dependency_links_path)
metadata_path = os.path.join(distinfo_path, "METADATA")
shutil.copy(pkginfo_path, metadata_path)
pkg_info_path = os.path.join(distinfo_path, "METADATA")
serialization_policy = EmailPolicy(
utf8=True,
mangle_from_=False,
max_line_length=0,
)
with open(pkg_info_path, "w", encoding="utf-8") as out:
Generator(out, policy=serialization_policy).flatten(pkg_info)
licenses_folder_path = os.path.join(distinfo_path, "licenses")
for license_path in self.license_paths:
safe_path = _safe_license_file(license_path)
dist_info_license_path = os.path.join(licenses_folder_path, safe_path)
os.makedirs(os.path.dirname(dist_info_license_path), exist_ok=True)
shutil.copy(license_path, dist_info_license_path)
filename = os.path.basename(license_path)
shutil.copy(license_path, os.path.join(distinfo_path, filename))

@@ -607,0 +649,0 @@ adios(egginfo_path)

@@ -27,3 +27,3 @@ from ..dist import Distribution

def build_libraries(self, libraries) -> None:
def build_libraries(self, libraries):
for lib_name, build_info in libraries:

@@ -33,5 +33,5 @@ sources = build_info.get('sources')

raise DistutilsSetupError(
f"in 'libraries' option (library '{lib_name}'), "
"in 'libraries' option (library '%s'), "
"'sources' must be present and must be "
"a list of source filenames"
"a list of source filenames" % lib_name
)

@@ -48,5 +48,5 @@ sources = sorted(list(sources))

raise DistutilsSetupError(
f"in 'libraries' option (library '{lib_name}'), "
"in 'libraries' option (library '%s'), "
"'obj_deps' must be a dictionary of "
"type 'source: list'"
"type 'source: list'" % lib_name
)

@@ -60,5 +60,5 @@ dependencies = []

raise DistutilsSetupError(
f"in 'libraries' option (library '{lib_name}'), "
"in 'libraries' option (library '%s'), "
"'obj_deps' must be a dictionary of "
"type 'source: list'"
"type 'source: list'" % lib_name
)

@@ -74,5 +74,5 @@

raise DistutilsSetupError(
f"in 'libraries' option (library '{lib_name}'), "
"in 'libraries' option (library '%s'), "
"'obj_deps' must be a dictionary of "
"type 'source: list'"
"type 'source: list'" % lib_name
)

@@ -79,0 +79,0 @@ src_deps.extend(extra_deps)

@@ -6,8 +6,6 @@ from __future__ import annotations

import sys
import textwrap
from collections.abc import Iterator
from importlib.machinery import EXTENSION_SUFFIXES
from importlib.util import cache_from_source as _compiled_file_name
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Iterator

@@ -79,2 +77,6 @@ from setuptools.dist import Distribution

def if_dl(s):
return s if have_rtld else ''
def get_abi3_suffix():

@@ -97,3 +99,3 @@ """Return the file extension for an abi3-compliant Extension()"""

"""Build extensions in build directory, then copy if --inplace"""
old_inplace, self.inplace = self.inplace, False
old_inplace, self.inplace = self.inplace, 0
_build_ext.run(self)

@@ -114,3 +116,3 @@ self.inplace = old_inplace

def copy_extensions_to_source(self) -> None:
def copy_extensions_to_source(self):
build_py = self.get_finalized_command('build_py')

@@ -172,3 +174,3 @@ for ext in self.extensions:

"Configuration variable EXT_SUFFIX not found for this platform "
"and environment variable SETUPTOOLS_EXT_SUFFIX is missing"
+ "and environment variable SETUPTOOLS_EXT_SUFFIX is missing"
)

@@ -197,3 +199,3 @@ so_ext = ext_suffix

def finalize_options(self) -> None:
def finalize_options(self):
_build_ext.finalize_options(self)

@@ -254,3 +256,3 @@ self.extensions = self.extensions or []

# hack so distutils' build_extension() builds a library instead
compiler.link_shared_object = link_shared_object.__get__(compiler) # type: ignore[method-assign]
compiler.link_shared_object = link_shared_object.__get__(compiler)

@@ -262,3 +264,3 @@ def get_export_symbols(self, ext):

def build_extension(self, ext) -> None:
def build_extension(self, ext):
ext._convert_pyx_sources_to_lang()

@@ -353,3 +355,3 @@ _compiler = self.compiler

def write_stub(self, output_dir, ext, compile=False) -> None:
def write_stub(self, output_dir, ext, compile=False):
stub_file = os.path.join(output_dir, *ext._full_name.split('.')) + '.py'

@@ -364,30 +366,26 @@ self._write_stub_file(stub_file, ext, compile)

with open(stub_file, 'w', encoding="utf-8") as f:
content = (
textwrap.dedent(f"""
def __bootstrap__():
global __bootstrap__, __file__, __loader__
import sys, os, importlib.resources as irs, importlib.util
#rtld import dl
with irs.files(__name__).joinpath(
{os.path.basename(ext._file_name)!r}) as __file__:
del __bootstrap__
if '__loader__' in globals():
del __loader__
#rtld old_flags = sys.getdlopenflags()
old_dir = os.getcwd()
try:
os.chdir(os.path.dirname(__file__))
#rtld sys.setdlopenflags(dl.RTLD_NOW)
spec = importlib.util.spec_from_file_location(
__name__, __file__)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
finally:
#rtld sys.setdlopenflags(old_flags)
os.chdir(old_dir)
__bootstrap__()
""")
.lstrip()
.replace('#rtld', '#rtld' * (not have_rtld))
)
content = '\n'.join([
"def __bootstrap__():",
" global __bootstrap__, __file__, __loader__",
" import sys, os, pkg_resources, importlib.util" + if_dl(", dl"),
" __file__ = pkg_resources.resource_filename"
"(__name__,%r)" % os.path.basename(ext._file_name),
" del __bootstrap__",
" if '__loader__' in globals():",
" del __loader__",
if_dl(" old_flags = sys.getdlopenflags()"),
" old_dir = os.getcwd()",
" try:",
" os.chdir(os.path.dirname(__file__))",
if_dl(" sys.setdlopenflags(dl.RTLD_NOW)"),
" spec = importlib.util.spec_from_file_location(",
" __name__, __file__)",
" mod = importlib.util.module_from_spec(spec)",
" spec.loader.exec_module(mod)",
" finally:",
if_dl(" sys.setdlopenflags(old_flags)"),
" os.chdir(old_dir)",
"__bootstrap__()",
"", # terminal \n
])
f.write(content)

@@ -430,3 +428,3 @@ if compile:

target_lang=None,
) -> None:
):
self.link(

@@ -466,3 +464,3 @@ self.SHARED_LIBRARY,

target_lang=None,
) -> None:
):
# XXX we need to either disallow these attrs on Library instances,

@@ -476,3 +474,3 @@ # or warn/abort here if set, or something...

output_dir, filename = os.path.split(output_libname)
basename, _ext = os.path.splitext(filename)
basename, ext = os.path.splitext(filename)
if self.library_filename("x").startswith('lib'):

@@ -479,0 +477,0 @@ # strip 'lib' prefix; this is kludgy if some platform uses

@@ -8,10 +8,11 @@ from __future__ import annotations

import textwrap
from collections.abc import Iterable, Iterator
from functools import partial
from glob import glob
from pathlib import Path
from typing import Iterable, Iterator
from more_itertools import unique_everseen
from .._path import StrPath, StrPathT
from setuptools._path import StrPath
from ..dist import Distribution

@@ -27,3 +28,3 @@ from ..warnings import SetuptoolsDeprecationWarning

def make_writable(target) -> None:
def make_writable(target):
os.chmod(target, os.stat(target).st_mode | stat.S_IWRITE)

@@ -44,3 +45,3 @@

editable_mode: bool = False
existing_egg_info_dir: StrPath | None = None #: Private API, internal use only.
existing_egg_info_dir: str | None = None #: Private API, internal use only.

@@ -53,7 +54,8 @@ def finalize_options(self):

del self.__dict__['data_files']
self.__updated_files = []
def copy_file( # type: ignore[override] # No overload, no bytes support
def copy_file( # type: ignore[override] # No overload, str support only
self,
infile: StrPath,
outfile: StrPathT,
outfile: StrPath,
preserve_mode: bool = True,

@@ -63,12 +65,12 @@ preserve_times: bool = True,

level: object = 1,
) -> tuple[StrPathT | str, bool]:
):
# Overwrite base class to allow using links
if link:
infile = str(Path(infile).resolve())
outfile = str(Path(outfile).resolve()) # type: ignore[assignment] # Re-assigning a str when outfile is StrPath is ok
return super().copy_file( # pyright: ignore[reportReturnType] # pypa/distutils#309
outfile = str(Path(outfile).resolve())
return super().copy_file(
infile, outfile, preserve_mode, preserve_times, link, level
)
def run(self) -> None:
def run(self):
"""Build modules, packages, and copy data files to build directory"""

@@ -96,2 +98,8 @@ if not (self.py_modules or self.packages) or self.editable_mode:

def build_module(self, module, module_file, package):
outfile, copied = orig.build_py.build_module(self, module, module_file, package)
if copied:
self.__updated_files.append(outfile)
return outfile, copied
def _get_data_files(self):

@@ -173,3 +181,3 @@ """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""

def build_package_data(self) -> None:
def build_package_data(self):
"""Copy data files into build directory"""

@@ -181,7 +189,7 @@ for target, srcfile in self._get_package_data_output_mapping():

def analyze_manifest(self) -> None:
self.manifest_files: dict[str, list[str]] = {}
def analyze_manifest(self):
self.manifest_files = mf = {}
if not self.distribution.include_package_data:
return
src_dirs: dict[str, str] = {}
src_dirs = {}
for package in self.packages or ():

@@ -192,3 +200,3 @@ # Locate package source directory

if (
self.existing_egg_info_dir
getattr(self, 'existing_egg_info_dir', None)
and Path(self.existing_egg_info_dir, "SOURCES.txt").exists()

@@ -222,7 +230,5 @@ ):

check.warn(importable)
self.manifest_files.setdefault(src_dirs[d], []).append(path)
mf.setdefault(src_dirs[d], []).append(path)
def _filter_build_files(
self, files: Iterable[str], egg_info: StrPath
) -> Iterator[str]:
def _filter_build_files(self, files: Iterable[str], egg_info: str) -> Iterator[str]:
"""

@@ -246,3 +252,3 @@ ``build_meta`` may try to create egg_info outside of the project directory,

def get_data_files(self) -> None:
def get_data_files(self):
pass # Lazily compute data files in _get_data_files() function.

@@ -273,6 +279,6 @@

raise distutils.errors.DistutilsError(
f"Namespace package problem: {package} is a namespace package, but "
"Namespace package problem: %s is a namespace package, but "
"its\n__init__.py does not call declare_namespace()! Please "
'fix it.\n(See the setuptools manual under '
'"Namespace Packages" for details.)\n"'
'"Namespace Packages" for details.)\n"' % (package,)
)

@@ -279,0 +285,0 @@ return init_py

@@ -88,11 +88,11 @@ from __future__ import annotations

def initialize_options(self) -> None:
def initialize_options(self):
"""(Required by the original :class:`setuptools.Command` interface)"""
...
def finalize_options(self) -> None:
def finalize_options(self):
"""(Required by the original :class:`setuptools.Command` interface)"""
...
def run(self) -> None:
def run(self):
"""(Required by the original :class:`setuptools.Command` interface)"""

@@ -99,0 +99,0 @@ ...

@@ -1,55 +0,195 @@

import site
import subprocess
import sys
import glob
import os
from setuptools import Command
from setuptools.warnings import SetuptoolsDeprecationWarning
import setuptools
from setuptools import _normalization, _path, namespaces
from setuptools.command.easy_install import easy_install
from ..unicode_utils import _read_utf8_with_fallback
class develop(Command):
from distutils import log
from distutils.errors import DistutilsOptionError
from distutils.util import convert_path
class develop(namespaces.DevelopInstaller, easy_install):
"""Set up package for development"""
user_options = [
("install-dir=", "d", "install package to DIR"),
('no-deps', 'N', "don't install dependencies"),
('user', None, f"install in user site-package '{site.USER_SITE}'"),
('prefix=', None, "installation prefix"),
("index-url=", "i", "base URL of Python Package Index"),
description = "install package in 'development mode'"
user_options = easy_install.user_options + [
("uninstall", "u", "Uninstall this source package"),
("egg-path=", None, "Set the path to be used in the .egg-link file"),
]
boolean_options = [
'no-deps',
'user',
]
install_dir = None
no_deps = False
user = False
prefix = None
index_url = None
boolean_options = easy_install.boolean_options + ['uninstall']
command_consumes_arguments = False # override base
def run(self):
cmd = (
[sys.executable, '-m', 'pip', 'install', '-e', '.', '--use-pep517']
+ ['--target', self.install_dir] * bool(self.install_dir)
+ ['--no-deps'] * self.no_deps
+ ['--user'] * self.user
+ ['--prefix', self.prefix] * bool(self.prefix)
+ ['--index-url', self.index_url] * bool(self.index_url)
)
subprocess.check_call(cmd)
if self.uninstall:
self.multi_version = True
self.uninstall_link()
self.uninstall_namespaces()
else:
self.install_for_development()
self.warn_deprecated_options()
def initialize_options(self):
DevelopDeprecationWarning.emit()
self.uninstall = None
self.egg_path = None
easy_install.initialize_options(self)
self.setup_path = None
self.always_copy_from = '.' # always copy eggs installed in curdir
def finalize_options(self) -> None:
pass
def finalize_options(self):
import pkg_resources
ei = self.get_finalized_command("egg_info")
self.args = [ei.egg_name]
class DevelopDeprecationWarning(SetuptoolsDeprecationWarning):
_SUMMARY = "develop command is deprecated."
_DETAILS = """
Please avoid running ``setup.py`` and ``develop``.
Instead, use standards-based tools like pip or uv.
easy_install.finalize_options(self)
self.expand_basedirs()
self.expand_dirs()
# pick up setup-dir .egg files only: no .egg-info
self.package_index.scan(glob.glob('*.egg'))
egg_link_fn = (
_normalization.filename_component_broken(ei.egg_name) + '.egg-link'
)
self.egg_link = os.path.join(self.install_dir, egg_link_fn)
self.egg_base = ei.egg_base
if self.egg_path is None:
self.egg_path = os.path.abspath(ei.egg_base)
target = _path.normpath(self.egg_base)
egg_path = _path.normpath(os.path.join(self.install_dir, self.egg_path))
if egg_path != target:
raise DistutilsOptionError(
"--egg-path must be a relative path from the install"
" directory to " + target
)
# Make a distribution for the package's source
self.dist = pkg_resources.Distribution(
target,
pkg_resources.PathMetadata(target, os.path.abspath(ei.egg_info)),
project_name=ei.egg_name,
)
self.setup_path = self._resolve_setup_path(
self.egg_base,
self.install_dir,
self.egg_path,
)
@staticmethod
def _resolve_setup_path(egg_base, install_dir, egg_path):
"""
Generate a path from egg_base back to '.' where the
setup script resides and ensure that path points to the
setup path from $install_dir/$egg_path.
"""
path_to_setup = egg_base.replace(os.sep, '/').rstrip('/')
if path_to_setup != os.curdir:
path_to_setup = '../' * (path_to_setup.count('/') + 1)
resolved = _path.normpath(os.path.join(install_dir, egg_path, path_to_setup))
curdir = _path.normpath(os.curdir)
if resolved != curdir:
raise DistutilsOptionError(
"Can't get a consistent path to setup script from"
" installation directory",
resolved,
curdir,
)
return path_to_setup
def install_for_development(self):
self.run_command('egg_info')
# Build extensions in-place
self.reinitialize_command('build_ext', inplace=True)
self.run_command('build_ext')
if setuptools.bootstrap_install_from:
self.easy_install(setuptools.bootstrap_install_from)
setuptools.bootstrap_install_from = None
self.install_namespaces()
# create an .egg-link in the installation dir, pointing to our egg
log.info("Creating %s (link to %s)", self.egg_link, self.egg_base)
if not self.dry_run:
with open(self.egg_link, "w", encoding="utf-8") as f:
f.write(self.egg_path + "\n" + self.setup_path)
# postprocess the installed distro, fixing up .pth, installing scripts,
# and handling requirements
self.process_distribution(None, self.dist, not self.no_deps)
def uninstall_link(self):
if os.path.exists(self.egg_link):
log.info("Removing %s (link to %s)", self.egg_link, self.egg_base)
contents = [
line.rstrip()
for line in _read_utf8_with_fallback(self.egg_link).splitlines()
]
if contents not in ([self.egg_path], [self.egg_path, self.setup_path]):
log.warn("Link points to %s: uninstall aborted", contents)
return
if not self.dry_run:
os.unlink(self.egg_link)
if not self.dry_run:
self.update_pth(self.dist) # remove any .pth link to us
if self.distribution.scripts:
# XXX should also check for entry point scripts!
log.warn("Note: you must uninstall or replace scripts manually!")
def install_egg_scripts(self, dist):
if dist is not self.dist:
# Installing a dependency, so fall back to normal behavior
return easy_install.install_egg_scripts(self, dist)
# create wrapper scripts in the script dir, pointing to dist.scripts
# new-style...
self.install_wrapper_scripts(dist)
# ...and old-style
for script_name in self.distribution.scripts or []:
script_path = os.path.abspath(convert_path(script_name))
script_name = os.path.basename(script_path)
script_text = _read_utf8_with_fallback(script_path)
self.install_script(dist, script_name, script_text, script_path)
return None
def install_wrapper_scripts(self, dist):
dist = VersionlessRequirement(dist)
return easy_install.install_wrapper_scripts(self, dist)
class VersionlessRequirement:
"""
_SEE_URL = "https://github.com/pypa/setuptools/issues/917"
_DUE_DATE = 2025, 10, 31
Adapt a pkg_resources.Distribution to simply return the project
name as the 'requirement' so that scripts will work across
multiple versions.
>>> from pkg_resources import Distribution
>>> dist = Distribution(project_name='foo', version='1.0')
>>> str(dist.as_requirement())
'foo==1.0'
>>> adapted_dist = VersionlessRequirement(dist)
>>> str(adapted_dist.as_requirement())
'foo'
"""
def __init__(self, dist):
self.__dist = dist
def __getattr__(self, name: str):
return getattr(self.__dist, name)
def as_requirement(self):
return self.project_name

@@ -13,3 +13,2 @@ """

from .. import _normalization
from .._shutil import rmdir as _rm
from .egg_info import egg_info as egg_info_cls

@@ -53,3 +52,3 @@

def finalize_options(self) -> None:
def finalize_options(self):
dist = self.distribution

@@ -94,3 +93,3 @@ project_dir = dist.src_root or os.curdir

def run(self) -> None:
def run(self):
self.output_dir.mkdir(parents=True, exist_ok=True)

@@ -101,3 +100,3 @@ self.egg_info.run()

log.info(f"creating '{os.path.abspath(self.dist_info_dir)}'")
log.info("creating '{}'".format(os.path.abspath(self.dist_info_dir)))
bdist_wheel = self.get_finalized_command('bdist_wheel')

@@ -108,1 +107,6 @@

bdist_wheel.egg2dist(egg_info_dir, self.dist_info_dir)
def _rm(dir_name, **opts):
if os.path.isdir(dir_name):
shutil.rmtree(dir_name, **opts)

@@ -20,3 +20,2 @@ """

import traceback
from collections.abc import Iterable, Iterator, Mapping
from contextlib import suppress

@@ -28,11 +27,10 @@ from enum import Enum

from tempfile import TemporaryDirectory
from types import TracebackType
from typing import TYPE_CHECKING, Protocol, TypeVar, cast
from typing import TYPE_CHECKING, Iterable, Iterator, Mapping, Protocol, TypeVar, cast
from .. import Command, _normalization, _path, _shutil, errors, namespaces
from .. import Command, _normalization, _path, errors, namespaces
from .._path import StrPath
from ..compat import py310, py312
from ..compat import py312
from ..discovery import find_package_path
from ..dist import Distribution
from ..warnings import InformationOnly, SetuptoolsDeprecationWarning
from ..warnings import InformationOnly, SetuptoolsDeprecationWarning, SetuptoolsWarning
from .build import build as build_cls

@@ -125,3 +123,3 @@ from .build_py import build_py as build_py_cls

def finalize_options(self) -> None:
def finalize_options(self):
dist = self.distribution

@@ -132,3 +130,3 @@ self.project_dir = dist.src_root or os.curdir

def run(self) -> None:
def run(self):
try:

@@ -144,10 +142,6 @@ self.dist_dir.mkdir(exist_ok=True)

self._create_wheel_file(bdist_wheel)
except Exception as ex:
except Exception:
traceback.print_exc()
project = self.distribution.name or self.distribution.get_name()
py310.add_note(
ex,
f"An error occurred when building editable wheel for {project}.\n"
"See debugging tips in: "
"https://setuptools.pypa.io/en/latest/userguide/development_mode.html#debugging-tips",
)
_DebuggingTips.emit(project=project)
raise

@@ -223,7 +217,2 @@

# For portability, ensure scripts are built with #!python shebang
# pypa/setuptools#4863
build_scripts = dist.get_command_obj("build_scripts")
build_scripts.executable = 'python'
install_scripts = cast(

@@ -296,3 +285,3 @@ install_scripts_cls, dist.get_command_obj("install_scripts")

"""
# TODO: Once plugins/customizations had the chance to catch up, replace
# TODO: Once plugins/customisations had the chance to catch up, replace
# `self._run_build_subcommands()` with `self.run_command("build")`.

@@ -328,3 +317,3 @@ # Also remove _safely_run, TestCustomBuildPy. Suggested date: Aug/2023.

For the time being `setuptools` will silence this error and ignore
the faulty command, but this behavior will change in future versions.
the faulty command, but this behaviour will change in future versions.
""",

@@ -401,9 +390,9 @@ # TODO: define due_date

self, wheel: WheelFile, files: list[str], mapping: Mapping[str, str]
) -> object: ...
): ...
def __enter__(self) -> Self: ...
def __exit__(
self,
_exc_type: type[BaseException] | None,
_exc_value: BaseException | None,
_traceback: TracebackType | None,
_exc_type: object,
_exc_value: object,
_traceback: object,
) -> object: ...

@@ -413,3 +402,3 @@

class _StaticPth:
def __init__(self, dist: Distribution, name: str, path_entries: list[Path]) -> None:
def __init__(self, dist: Distribution, name: str, path_entries: list[Path]):
self.dist = dist

@@ -458,3 +447,3 @@ self.name = name

build_lib: StrPath,
) -> None:
):
self.auxiliary_dir = Path(auxiliary_dir)

@@ -519,3 +508,3 @@ self.build_lib = Path(build_lib).resolve()

class _TopLevelFinder:
def __init__(self, dist: Distribution, name: str) -> None:
def __init__(self, dist: Distribution, name: str):
self.dist = dist

@@ -530,3 +519,3 @@ self.name = name

namespaces_ = dict(
namespaces_: dict[str, list[str]] = dict(
chain(

@@ -589,3 +578,3 @@ _find_namespaces(self.dist.packages or [], roots),

and ignore warnings (see python/cpython#77102, pypa/setuptools#3937).
This function tries to simulate this behavior without having to create an
This function tries to simulate this behaviour without having to create an
actual file, in a way that supports a range of active Python versions.

@@ -799,3 +788,3 @@ (There seems to be some variety in the way different version of Python handle

"""Create a directory ensured to be empty. Existing files may be removed."""
_shutil.rmtree(dir_, ignore_errors=True)
shutil.rmtree(dir_, ignore_errors=True)
os.makedirs(dir_)

@@ -806,3 +795,3 @@ return dir_

class _NamespaceInstaller(namespaces.Installer):
def __init__(self, distribution, installation_dir, editable_name, src_root) -> None:
def __init__(self, distribution, installation_dir, editable_name, src_root):
self.distribution = distribution

@@ -812,3 +801,3 @@ self.src_root = src_root

self.editable_name = editable_name
self.outputs: list[str] = []
self.outputs = []
self.dry_run = False

@@ -926,1 +915,27 @@

"""File system does not seem to support either symlinks or hard links."""
class _DebuggingTips(SetuptoolsWarning):
_SUMMARY = "Problem in editable installation."
_DETAILS = """
An error happened while installing `{project}` in editable mode.
The following steps are recommended to help debug this problem:
- Try to install the project normally, without using the editable mode.
Does the error still persist?
(If it does, try fixing the problem before attempting the editable mode).
- If you are using binary extensions, make sure you have all OS-level
dependencies installed (e.g. compilers, toolchains, binary libraries, ...).
- Try the latest version of setuptools (maybe the error was already fixed).
- If you (or your project dependencies) are using any setuptools extension
or customization, make sure they support the editable mode.
After following the steps above, if the problem still persists and
you think this is related to how setuptools handles editable installations,
please submit a reproducible example
(see https://stackoverflow.com/help/minimal-reproducible-example) to:
https://github.com/pypa/setuptools/issues
"""
_SEE_DOCS = "userguide/development_mode.html"

@@ -10,3 +10,2 @@ """setuptools.command.egg_info

import time
from collections.abc import Callable

@@ -36,3 +35,3 @@ import packaging

PY_MAJOR = f'{sys.version_info.major}.{sys.version_info.minor}'
PY_MAJOR = '{}.{}'.format(*sys.version_info)

@@ -53,3 +52,3 @@

sep = re.escape(os.sep)
valid_char = f'[^{sep}]'
valid_char = '[^%s]' % (sep,)

@@ -66,3 +65,3 @@ for c, chunk in enumerate(chunks):

# Match '(name/)*'
pat += f'(?:{valid_char}+{sep})*'
pat += '(?:%s+%s)*' % (valid_char, sep)
continue # Break here as the whole path component has been handled

@@ -109,3 +108,3 @@

char_class += re.escape(inner)
pat += f'[{char_class}]'
pat += '[%s]' % (char_class,)

@@ -204,3 +203,3 @@ # Skip to the end ]

@property
def tag_svn_revision(self) -> None:
def tag_svn_revision(self):
pass

@@ -214,3 +213,3 @@

def save_version_info(self, filename) -> None:
def save_version_info(self, filename):
"""

@@ -226,3 +225,3 @@ Materialize the value of date into the

def finalize_options(self) -> None:
def finalize_options(self):
# Note: we need to capture the current value returned

@@ -242,3 +241,4 @@ # by `self.tagged_version()`, so we can later update

raise distutils.errors.DistutilsOptionError(
f"Invalid distribution name or version syntax: {self.egg_name}-{self.egg_version}"
"Invalid distribution name or version syntax: %s-%s"
% (self.egg_name, self.egg_version)
) from e

@@ -264,3 +264,3 @@

def write_or_delete_file(self, what, filename, data, force: bool = False) -> None:
def write_or_delete_file(self, what, filename, data, force: bool = False):
"""Write `data` to `filename` or delete if empty

@@ -283,3 +283,3 @@

def write_file(self, what, filename, data) -> None:
def write_file(self, what, filename, data):
"""Write `data` to `filename` (if not a dry run) after announcing it

@@ -297,3 +297,3 @@

def delete_file(self, filename) -> None:
def delete_file(self, filename):
"""Delete `filename` (if not a dry run) after announcing it"""

@@ -304,3 +304,3 @@ log.info("deleting %s", filename)

def run(self) -> None:
def run(self):
# Pre-load to avoid iterating over entry-points while an empty .egg-info

@@ -327,3 +327,3 @@ # exists in sys.path. See pypa/pyproject-hooks#206

def find_sources(self) -> None:
def find_sources(self):
"""Generate SOURCES.txt manifest file"""

@@ -341,9 +341,7 @@ manifest_filename = os.path.join(self.egg_info, "SOURCES.txt")

def __init__(
self, warn=None, debug_print=None, ignore_egg_info_dir: bool = False
) -> None:
def __init__(self, warn=None, debug_print=None, ignore_egg_info_dir: bool = False):
super().__init__(warn, debug_print)
self.ignore_egg_info_dir = ignore_egg_info_dir
def process_template_line(self, line) -> None:
def process_template_line(self, line):
# Parse the line: split it up, make sure the right number of words

@@ -356,3 +354,3 @@ # is there, and return the relevant words. 'action' is always

action_map: dict[str, Callable] = {
action_map = {
'include': self.include,

@@ -493,3 +491,3 @@ 'exclude': self.exclude,

def append(self, item) -> None:
def append(self, item):
if item.endswith('\r'): # Fix older sdists built on Windows

@@ -502,3 +500,3 @@ item = item[:-1]

def extend(self, paths) -> None:
def extend(self, paths):
self.files.extend(filter(self._safe_path, paths))

@@ -522,3 +520,3 @@

if u_path is None:
log.warn(f"'{path}' in unexpected encoding -- skipping")
log.warn("'%s' in unexpected encoding -- skipping" % path)
return False

@@ -548,3 +546,3 @@

def initialize_options(self) -> None:
def initialize_options(self):
self.use_defaults = True

@@ -556,6 +554,6 @@ self.prune = True

def finalize_options(self) -> None:
def finalize_options(self):
pass
def run(self) -> None:
def run(self):
self.filelist = FileList(ignore_egg_info_dir=self.ignore_egg_info_dir)

@@ -578,3 +576,3 @@ if not os.path.exists(self.manifest):

def write_manifest(self) -> None:
def write_manifest(self):
"""

@@ -588,6 +586,6 @@ Write the file list in 'self.filelist' to the manifest file

files = [self._manifest_normalize(f) for f in self.filelist.files]
msg = f"writing manifest file '{self.manifest}'"
msg = "writing manifest file '%s'" % self.manifest
self.execute(write_file, (self.manifest, files), msg)
def warn(self, msg) -> None:
def warn(self, msg):
if not self._should_suppress_warning(msg):

@@ -603,3 +601,3 @@ sdist.warn(self, msg)

def add_defaults(self) -> None:
def add_defaults(self):
sdist.add_defaults(self)

@@ -622,3 +620,3 @@ self.filelist.append(self.template)

def add_license_files(self) -> None:
def add_license_files(self):
license_files = self.distribution.metadata.license_files or []

@@ -662,3 +660,3 @@ for lf in license_files:

def write_file(filename, contents) -> None:
def write_file(filename, contents):
"""Create a file with the specified name and write 'contents' (a

@@ -676,3 +674,3 @@ sequence of strings without line terminators) to it.

def write_pkg_info(cmd, basename, filename) -> None:
def write_pkg_info(cmd, basename, filename):
log.info("writing %s", filename)

@@ -685,2 +683,4 @@ if not cmd.dry_run:

try:
# write unescaped data to PKG-INFO, so older pkg_resources
# can still parse it
metadata.write_pkg_info(cmd.egg_info)

@@ -695,3 +695,3 @@ finally:

def warn_depends_obsolete(cmd, basename, filename) -> None:
def warn_depends_obsolete(cmd, basename, filename):
"""

@@ -711,3 +711,3 @@ Unused: left to avoid errors when updating (from source) from <= 67.8.

def write_toplevel_names(cmd, basename, filename) -> None:
def write_toplevel_names(cmd, basename, filename):
pkgs = dict.fromkeys([

@@ -719,7 +719,7 @@ k.split('.', 1)[0] for k in cmd.distribution.iter_distribution_names()

def overwrite_arg(cmd, basename, filename) -> None:
def overwrite_arg(cmd, basename, filename):
write_arg(cmd, basename, filename, True)
def write_arg(cmd, basename, filename, force: bool = False) -> None:
def write_arg(cmd, basename, filename, force: bool = False):
argname = os.path.splitext(basename)[0]

@@ -732,3 +732,3 @@ value = getattr(cmd.distribution, argname, None)

def write_entries(cmd, basename, filename) -> None:
def write_entries(cmd, basename, filename):
eps = _entry_points.load(cmd.distribution.entry_points)

@@ -735,0 +735,0 @@ defn = _entry_points.render(eps)

@@ -23,3 +23,3 @@ import os

def finalize_options(self) -> None:
def finalize_options(self):
self.set_undefined_options('install_lib', ('install_dir', 'install_dir'))

@@ -30,5 +30,5 @@ ei_cmd = self.get_finalized_command("egg_info")

self.target = os.path.join(self.install_dir, basename)
self.outputs: list[str] = []
self.outputs = []
def run(self) -> None:
def run(self):
self.run_command('egg_info')

@@ -41,3 +41,3 @@ if os.path.isdir(self.target) and not os.path.islink(self.target):

ensure_directory(self.target)
self.execute(self.copytree, (), f"Copying {self.source} to {self.target}")
self.execute(self.copytree, (), "Copying %s to %s" % (self.source, self.target))
self.install_namespaces()

@@ -48,3 +48,3 @@

def copytree(self) -> None:
def copytree(self):
# Copy the .egg-info tree to site-packages

@@ -51,0 +51,0 @@ def skimmer(src, dst):

@@ -18,3 +18,3 @@ from __future__ import annotations

def run(self) -> None:
def run(self):
self.build()

@@ -56,3 +56,3 @@ outfiles = self.install()

yield pkg_name
pkg_name, _sep, _child = pkg_name.rpartition('.')
pkg_name, sep, child = pkg_name.rpartition('.')

@@ -106,5 +106,3 @@ def _get_SVEM_NSPs(self):

) -> list[str]:
assert preserve_mode
assert preserve_times
assert not preserve_symlinks
assert preserve_mode and preserve_times and not preserve_symlinks
exclude = self.get_exclusions()

@@ -111,0 +109,0 @@

@@ -18,3 +18,3 @@ from __future__ import annotations

def initialize_options(self) -> None:
def initialize_options(self):
orig.install_scripts.initialize_options(self)

@@ -36,10 +36,16 @@ self.no_ep = False

# Delay import side-effects
from .. import _scripts
from .._importlib import metadata
from pkg_resources import Distribution, PathMetadata
from . import easy_install as ei
ei_cmd = self.get_finalized_command("egg_info")
dist = metadata.Distribution.at(path=ei_cmd.egg_info)
dist = Distribution(
ei_cmd.egg_base,
PathMetadata(ei_cmd.egg_base, ei_cmd.egg_info),
ei_cmd.egg_name,
ei_cmd.egg_version,
)
bs_cmd = self.get_finalized_command('build_scripts')
exec_param = getattr(bs_cmd, 'executable', None)
writer = _scripts.ScriptWriter
writer = ei.ScriptWriter
if exec_param == sys.executable:

@@ -55,5 +61,5 @@ # In case the path to the Python executable contains a space, wrap

def write_script(self, script_name, contents, mode: str = "t", *ignored) -> None:
def write_script(self, script_name, contents, mode: str = "t", *ignored):
"""Write an executable file to the scripts directory"""
from .._shutil import attempt_chmod_verbose as chmod, current_umask
from setuptools.command.easy_install import chmod, current_umask

@@ -60,0 +66,0 @@ log.info("Installing %s script to %s", script_name, self.install_dir)

from __future__ import annotations
import glob
import inspect
import platform
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, ClassVar
from typing import Any, ClassVar, cast
import setuptools
from ..dist import Distribution
from ..warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning
from .bdist_egg import bdist_egg as bdist_egg_cls

@@ -14,20 +18,7 @@ import distutils.command.install as orig

if TYPE_CHECKING:
# This is only used for a type-cast, don't import at runtime or it'll cause deprecation warnings
from .easy_install import easy_install as easy_install_cls
else:
easy_install_cls = None
# Prior to numpy 1.9, NumPy relies on the '_install' name, so provide it for
# now. See https://github.com/pypa/setuptools/issues/199/
_install = orig.install
def __getattr__(name: str): # pragma: no cover
if name == "_install":
SetuptoolsDeprecationWarning.emit(
"`setuptools.command._install` was an internal implementation detail "
"that was left in for numpy<1.9 support.",
due_date=(2025, 5, 2), # Originally added on 2024-11-01
)
return orig.install
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
class install(orig.install):

@@ -67,3 +58,5 @@ """Use easy_install to install the package, w/dependencies"""

see_url="https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html",
due_date=(2025, 10, 31),
# TODO: Document how to bootstrap setuptools without install
# (e.g. by unziping the wheel file)
# and then add a due_date to this warning.
)

@@ -75,3 +68,3 @@

def finalize_options(self) -> None:
def finalize_options(self):
super().finalize_options()

@@ -97,2 +90,15 @@ if self.root:

def run(self):
# Explicit request for old-style install? Just do it
if self.old_and_unmanageable or self.single_version_externally_managed:
return super().run()
if not self._called_from_setup(inspect.currentframe()):
# Run in backward-compatibility mode to support bdist_* commands.
super().run()
else:
self.do_egg_install()
return None
@staticmethod

@@ -131,3 +137,30 @@ def _called_from_setup(run_frame):

def do_egg_install(self):
easy_install = self.distribution.get_command_class('easy_install')
cmd = easy_install(
self.distribution,
args="x",
root=self.root,
record=self.record,
)
cmd.ensure_finalized() # finalize before bdist_egg munges install cmd
cmd.always_copy_from = '.' # make sure local-dir eggs get installed
# pick up setup-dir .egg files only: no .egg-info
cmd.package_index.scan(glob.glob('*.egg'))
self.run_command('bdist_egg')
bdist_egg = cast(bdist_egg_cls, self.distribution.get_command_obj('bdist_egg'))
args = [bdist_egg.egg_output]
if setuptools.bootstrap_install_from:
# Bootstrap self-installation of setuptools
args.insert(0, setuptools.bootstrap_install_from)
cmd.args = args
cmd.run(show_deprecation=False)
setuptools.bootstrap_install_from = None
# XXX Python 3.1 doesn't see _nc if this is inside the class

@@ -134,0 +167,0 @@ install.sub_commands = [

from __future__ import annotations
import os
from typing import ClassVar
import shutil
from .. import Command, _shutil
from setuptools import Command

@@ -23,3 +23,3 @@ from distutils import log

boolean_options: ClassVar[list[str]] = []
boolean_options: list[str] = []

@@ -31,3 +31,3 @@ def initialize_options(self):

def finalize_options(self) -> None:
def finalize_options(self):
if self.match is None:

@@ -48,3 +48,3 @@ raise DistutilsOptionError(

def run(self) -> None:
def run(self):
self.run_command("egg_info")

@@ -66,4 +66,4 @@ from glob import glob

if os.path.isdir(f):
_shutil.rmtree(f)
shutil.rmtree(f)
else:
os.unlink(f)

@@ -9,5 +9,5 @@ from setuptools.command.setopt import edit_config, option_base

def run(self) -> None:
def run(self):
dist = self.distribution
settings: dict[str, dict[str, str]] = {}
settings = {}

@@ -14,0 +14,0 @@ for cmd in dist.command_options:

@@ -33,3 +33,3 @@ from __future__ import annotations

'k',
"keep the distribution tree around after creating archive file(s)",
"keep the distribution tree around after creating " + "archive file(s)",
),

@@ -57,5 +57,5 @@ (

README_EXTENSIONS = ['', '.rst', '.txt', '.md']
READMES = tuple(f'README{ext}' for ext in README_EXTENSIONS)
READMES = tuple('README{0}'.format(ext) for ext in README_EXTENSIONS)
def run(self) -> None:
def run(self):
self.run_command('egg_info')

@@ -79,6 +79,6 @@ ei_cmd = self.get_finalized_command('egg_info')

def initialize_options(self) -> None:
def initialize_options(self):
orig.sdist.initialize_options(self)
def make_distribution(self) -> None:
def make_distribution(self):
"""

@@ -111,3 +111,3 @@ Workaround for #516

def add_defaults(self) -> None:
def add_defaults(self):
super().add_defaults()

@@ -165,3 +165,3 @@ self._add_defaults_build_sub_commands()

def prune_file_list(self) -> None:
def prune_file_list(self):
super().prune_file_list()

@@ -172,3 +172,3 @@ # Prevent accidental inclusion of test-related cache dirs at the project root

def check_readme(self) -> None:
def check_readme(self):
for f in self.READMES:

@@ -182,3 +182,3 @@ if os.path.exists(f):

def make_release_tree(self, base_dir, files) -> None:
def make_release_tree(self, base_dir, files):
orig.sdist.make_release_tree(self, base_dir, files)

@@ -212,8 +212,8 @@

manifest = open(self.manifest, 'rb')
for bytes_line in manifest:
for line in manifest:
# The manifest must contain UTF-8. See #303.
try:
line = bytes_line.decode('UTF-8')
line = line.decode('UTF-8')
except UnicodeDecodeError:
log.warn(f"{line!r} not UTF-8 decodable -- skipping")
log.warn("%r not UTF-8 decodable -- skipping" % line)
continue

@@ -220,0 +220,0 @@ # ignore comments and blank lines

@@ -26,3 +26,3 @@ import configparser

dot = os.name == 'posix' and '.' or ''
return os.path.expanduser(convert_path(f"~/{dot}pydistutils.cfg"))
return os.path.expanduser(convert_path("~/%spydistutils.cfg" % dot))
raise ValueError("config_file() type must be 'local', 'global', or 'user'", kind)

@@ -41,3 +41,3 @@

opts = configparser.RawConfigParser()
opts.optionxform = lambda optionstr: optionstr # type: ignore[method-assign] # overriding method
opts.optionxform = lambda x: x
_cfg_read_utf8_with_fallback(opts, filename)

@@ -131,3 +131,3 @@

def finalize_options(self) -> None:
def finalize_options(self):
option_base.finalize_options(self)

@@ -139,3 +139,3 @@ if self.command is None or self.option is None:

def run(self) -> None:
def run(self):
edit_config(

@@ -142,0 +142,0 @@ self.filename,

@@ -10,12 +10,1 @@ import sys

import tomli as tomllib
if sys.version_info >= (3, 11):
def add_note(ex, note):
ex.add_note(note)
else: # pragma: no cover
def add_note(ex, note):
vars(ex).setdefault('__notes__', []).append(note)

@@ -15,3 +15,2 @@ """Translation layer between pyproject config and setuptools distribution and

import os
from collections.abc import Mapping
from email.headerregistry import Address

@@ -22,9 +21,8 @@ from functools import partial, reduce

from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
from typing import TYPE_CHECKING, Any, Callable, Dict, Mapping, TypeVar, Union
from .. import _static
from .._path import StrPath
from ..errors import InvalidConfigError, RemovedConfigError
from ..errors import RemovedConfigError
from ..extension import Extension
from ..warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning
from ..warnings import SetuptoolsWarning

@@ -41,3 +39,3 @@ if TYPE_CHECKING:

EMPTY: Mapping = MappingProxyType({}) # Immutable dict-like
_ProjectReadmeValue: TypeAlias = Union[str, dict[str, str]]
_ProjectReadmeValue: TypeAlias = Union[str, Dict[str, str]]
_Correspondence: TypeAlias = Callable[["Distribution", Any, Union[StrPath, None]], None]

@@ -64,3 +62,2 @@ _T = TypeVar("_T")

dist._finalize_requires()
dist._finalize_license_expression()
dist._finalize_license_files()

@@ -74,7 +71,6 @@ finally:

def _apply_project_table(dist: Distribution, config: dict, root_dir: StrPath):
orig_config = config.get("project", {})
if not orig_config:
project_table = config.get("project", {}).copy()
if not project_table:
return # short-circuit
project_table = {k: _static.attempt_conversion(v) for k, v in orig_config.items()}
_handle_missing_dynamic(dist, project_table)

@@ -97,18 +93,2 @@ _unify_entry_points(project_table)

if "license-files" in tool_table:
if "license-files" in config.get("project", {}):
# https://github.com/pypa/setuptools/pull/4837#discussion_r2004983349
raise InvalidConfigError(
"'project.license-files' is defined already. "
"Remove 'tool.setuptools.license-files'."
)
pypa_guides = "guides/writing-pyproject-toml/#license-files"
SetuptoolsDeprecationWarning.emit(
"'tool.setuptools.license-files' is deprecated in favor of "
"'project.license-files' (available on setuptools>=77.0.0).",
see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",
due_date=(2026, 2, 18), # Warning introduced on 2025-02-18
)
for field, value in tool_table.items():

@@ -126,7 +106,3 @@ norm_key = json_compatible_key(field)

norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key)
corresp = TOOL_TABLE_CORRESPONDENCE.get(norm_key, norm_key)
if callable(corresp):
corresp(dist, value)
else:
_set_config(dist, corresp, value)
_set_config(dist, norm_key, value)

@@ -176,3 +152,3 @@ _copy_command_options(config, dist, filename)

if ext in _CONTENT_TYPES:
return _static.Str(_CONTENT_TYPES[ext])
return _CONTENT_TYPES[ext]

@@ -199,7 +175,6 @@ valid = ", ".join(f"{k} ({v})" for k, v in _CONTENT_TYPES.items())

# XXX: Is it completely safe to assume static?
_set_config(dist, "long_description", _static.Str(text))
_set_config(dist, "long_description", text)
if ctype:
_set_config(dist, "long_description_content_type", _static.Str(ctype))
_set_config(dist, "long_description_content_type", ctype)

@@ -210,27 +185,10 @@ if file:

def _license(dist: Distribution, val: str | dict, root_dir: StrPath | None):
def _license(dist: Distribution, val: dict, root_dir: StrPath | None):
from setuptools.config import expand
if isinstance(val, str):
if getattr(dist.metadata, "license", None):
SetuptoolsWarning.emit("`license` overwritten by `pyproject.toml`")
dist.metadata.license = None
_set_config(dist, "license_expression", _static.Str(val))
if "file" in val:
_set_config(dist, "license", expand.read_files([val["file"]], root_dir))
dist._referenced_files.add(val["file"])
else:
pypa_guides = "guides/writing-pyproject-toml/#license"
SetuptoolsDeprecationWarning.emit(
"`project.license` as a TOML table is deprecated",
"Please use a simple string containing a SPDX expression for "
"`project.license`. You can also use `project.license-files`. "
"(Both options available on setuptools>=77.0.0).",
see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",
due_date=(2026, 2, 18), # Introduced on 2025-02-18
)
if "file" in val:
# XXX: Is it completely safe to assume static?
value = expand.read_files([val["file"]], root_dir)
_set_config(dist, "license", _static.Str(value))
dist._referenced_files.add(val["file"])
else:
_set_config(dist, "license", _static.Str(val["text"]))
_set_config(dist, "license", val["text"])

@@ -251,5 +209,5 @@

if field:
_set_config(dist, kind, _static.Str(", ".join(field)))
_set_config(dist, kind, ", ".join(field))
if email_field:
_set_config(dist, f"{kind}_email", _static.Str(", ".join(email_field)))
_set_config(dist, f"{kind}_email", ", ".join(email_field))

@@ -262,5 +220,7 @@

def _python_requires(dist: Distribution, val: str, _root_dir: StrPath | None):
_set_config(dist, "python_requires", _static.SpecifierSet(val))
from packaging.specifiers import SpecifierSet
_set_config(dist, "python_requires", SpecifierSet(val))
def _dependencies(dist: Distribution, val: list, _root_dir: StrPath | None):

@@ -291,10 +251,5 @@ if getattr(dist, "install_requires", []):

def _identity(val: _T) -> _T:
return val
def _unify_entry_points(project_table: dict):
project = project_table
given = project.pop("entry-points", project.pop("entry_points", {}))
entry_points = dict(given) # Avoid problems with static
entry_points = project.pop("entry-points", project.pop("entry_points", {}))
renaming = {"scripts": "console_scripts", "gui_scripts": "gui_scripts"}

@@ -393,10 +348,2 @@ for key, value in list(project.items()): # eager to allow modifications

def _set_static_list_metadata(attr: str, dist: Distribution, val: list) -> None:
"""Apply distutils metadata validation but preserve "static" behaviour"""
meta = dist.metadata
setter, getter = getattr(meta, f"set_{attr}"), getattr(meta, f"get_{attr}")
setter(val)
setattr(meta, attr, _static.List(getter()))
def _attrgetter(attr):

@@ -455,8 +402,2 @@ """

}
TOOL_TABLE_CORRESPONDENCE = {
# Fields with corresponding core metadata need to be marked as static:
"obsoletes": partial(_set_static_list_metadata, "obsoletes"),
"provides": partial(_set_static_list_metadata, "provides"),
"platforms": partial(_set_static_list_metadata, "platforms"),
}

@@ -469,3 +410,2 @@ SETUPTOOLS_PATCHES = {

"license_files",
"license_expression",
}

@@ -483,5 +423,3 @@

"requires-python": _some_attrgetter("python_requires", "metadata.python_requires"),
"license": _some_attrgetter("metadata.license_expression", "metadata.license"),
# XXX: `license-file` is currently not considered in the context of `dynamic`.
# See TestPresetField.test_license_files_exempt_from_dynamic
"license": _attrgetter("metadata.license"),
"authors": _some_attrgetter("metadata.author", "metadata.author_email"),

@@ -502,17 +440,14 @@ "maintainers": _some_attrgetter("metadata.maintainer", "metadata.maintainer_email"),

# Fix improper setting: given in `setup.py`, but not listed in `dynamic`
# Use "immutable" data structures to avoid in-place modification.
# dict: pyproject name => value to which reset
"license": "",
# XXX: `license-file` is currently not considered in the context of `dynamic`.
# See TestPresetField.test_license_files_exempt_from_dynamic
"authors": _static.EMPTY_LIST,
"maintainers": _static.EMPTY_LIST,
"keywords": _static.EMPTY_LIST,
"classifiers": _static.EMPTY_LIST,
"urls": _static.EMPTY_DICT,
"entry-points": _static.EMPTY_DICT,
"scripts": _static.EMPTY_DICT,
"gui-scripts": _static.EMPTY_DICT,
"dependencies": _static.EMPTY_LIST,
"optional-dependencies": _static.EMPTY_DICT,
"license": {},
"authors": [],
"maintainers": [],
"keywords": [],
"classifiers": [],
"urls": {},
"entry-points": {},
"scripts": {},
"gui-scripts": {},
"dependencies": [],
"optional-dependencies": {},
}

@@ -519,0 +454,0 @@

@@ -27,9 +27,2 @@ """The purpose of this module is implement PEP 621 validations that are

class IncludedDependencyGroupMustExist(ValidationError):
_DESC = """An included dependency group must exist and must not be cyclic.
"""
__doc__ = _DESC
_URL = "https://peps.python.org/pep-0735/"
def validate_project_dynamic(pyproject: T) -> T:

@@ -60,25 +53,2 @@ project_table = pyproject.get("project", {})

def validate_include_depenency(pyproject: T) -> T:
dependency_groups = pyproject.get("dependency-groups", {})
for key, value in dependency_groups.items():
for each in value:
if (
isinstance(each, dict)
and (include_group := each.get("include-group"))
and include_group not in dependency_groups
):
raise IncludedDependencyGroupMustExist(
message=f"The included dependency group {include_group} doesn't exist",
value=each,
name=f"data.dependency_groups.{key}",
definition={
"description": cleandoc(IncludedDependencyGroupMustExist._DESC),
"see": IncludedDependencyGroupMustExist._URL,
},
rule="PEP 735",
)
# TODO: check for `include-group` cycles (can be conditional to graphlib)
return pyproject
EXTRA_VALIDATIONS = (validate_project_dynamic, validate_include_depenency)
EXTRA_VALIDATIONS = (validate_project_dynamic,)

@@ -167,7 +167,2 @@ """

downloaded: typing.Union[None, "Literal[False]", typing.Set[str]]
"""
None => not cached yet
False => unavailable
set => cached values
"""

@@ -177,2 +172,4 @@ def __init__(self) -> None:

self._skip_download = False
# None => not cached yet
# False => cache not available
self.__name__ = "trove_classifier" # Emulate a public function

@@ -359,3 +356,3 @@

module_parts = module.split(".")
identifiers = _chain(module_parts, obj.split(".")) if rest else iter(module_parts)
identifiers = _chain(module_parts, obj.split(".")) if rest else module_parts
return all(python_identifier(i.strip()) for i in identifiers)

@@ -382,25 +379,1 @@

return -(2**63) <= value < 2**63
try:
from packaging import licenses as _licenses
def SPDX(value: str) -> bool:
"""See :ref:`PyPA's License-Expression specification
<pypa:core-metadata-license-expression>` (added in :pep:`639`).
"""
try:
_licenses.canonicalize_license_expression(value)
return True
except _licenses.InvalidLicenseExpression:
return False
except ImportError: # pragma: no cover
_logger.warning(
"Could not find an up-to-date installation of `packaging`. "
"License expressions might not be validated. "
"To enforce validation, please install `packaging>=24.2`."
)
def SPDX(value: str) -> bool:
return True
The code contained in this directory was automatically generated using the
following command:
python -m validate_pyproject.pre_compile --output-dir=setuptools/config/_validate_pyproject --enable-plugins setuptools distutils --very-verbose -t setuptools=setuptools/config/setuptools.schema.json -t distutils=setuptools/config/distutils.schema.json
python -m validate_pyproject.pre_compile --output-dir=setuptools/config/_validate_pyproject --enable-plugins setuptools distutils --very-verbose -t distutils=setuptools/config/distutils.schema.json -t setuptools=setuptools/config/setuptools.schema.json

@@ -6,0 +6,0 @@ Please avoid changing it manually.

@@ -28,3 +28,2 @@ """Utility functions to expand configuration directives or special values

import sys
from collections.abc import Iterable, Iterator, Mapping
from configparser import ConfigParser

@@ -36,5 +35,4 @@ from glob import iglob

from types import ModuleType, TracebackType
from typing import TYPE_CHECKING, Any, Callable, TypeVar
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Mapping, TypeVar
from .. import _static
from .._path import StrPath, same_path as _same_path

@@ -58,3 +56,3 @@ from ..discovery import find_package_path

def __init__(self, name: str, spec: ModuleSpec) -> None:
def __init__(self, name: str, spec: ModuleSpec):
module = ast.parse(pathlib.Path(spec.origin).read_bytes()) # type: ignore[arg-type] # Let it raise an error on None

@@ -188,5 +186,3 @@ vars(self).update(locals())

try:
value = getattr(StaticModule(module_name, spec), attr_name)
# XXX: Is marking as static contents coming from modules too optimistic?
return _static.attempt_conversion(value)
return getattr(StaticModule(module_name, spec), attr_name)
except Exception:

@@ -339,3 +335,3 @@ # fallback to evaluate module

return '.'.join(map(str, _value))
return f'{_value}'
return '%s' % _value

@@ -366,5 +362,3 @@

def entry_points(
text: str, text_source: str = "entry-points"
) -> dict[str, dict[str, str]]:
def entry_points(text: str, text_source: str = "entry-points") -> dict[str, dict]:
"""Given the contents of entry-points file,

@@ -395,3 +389,3 @@ process it into a 2-level dictionary (``dict[str, dict[str, str]]``).

def __init__(self, distribution: Distribution) -> None:
def __init__(self, distribution: Distribution):
self._dist = distribution

@@ -443,3 +437,3 @@ self._called = False

def __init__(self, obtain_mapping_value: Callable[[], Mapping[_K, _V_co]]) -> None:
def __init__(self, obtain_mapping_value: Callable[[], Mapping[_K, _V_co]]):
self._obtain = obtain_mapping_value

@@ -446,0 +440,0 @@ self._value: Mapping[_K, _V_co] | None = None

@@ -16,7 +16,6 @@ """

import os
from collections.abc import Mapping
from contextlib import contextmanager
from functools import partial
from types import TracebackType
from typing import TYPE_CHECKING, Any, Callable
from typing import TYPE_CHECKING, Any, Callable, Mapping

@@ -180,3 +179,3 @@ from .._path import StrPath

dist: Distribution | None = None,
) -> None:
):
self.config = config

@@ -190,3 +189,3 @@ self.root_dir = root_dir or os.getcwd()

self._dist = dist
self._referenced_files = set[str]()
self._referenced_files: set[str] = set()

@@ -337,3 +336,3 @@ def _ensure_dist(self) -> Distribution:

self, dist: Distribution, package_dir: Mapping[str, str]
) -> dict[str, dict[str, Any]] | None:
) -> dict[str, dict] | None:
fields = ("entry-points", "scripts", "gui-scripts")

@@ -348,4 +347,3 @@ if not any(field in self.dynamic for field in fields):

groups = _expand.entry_points(text)
# Any is str | dict[str, str], but causes variance issues
expanded: dict[str, dict[str, Any]] = {"entry-points": groups}
expanded = {"entry-points": groups}

@@ -421,3 +419,3 @@ def _set_scripts(field: str, group: str):

self, distribution: Distribution, project_cfg: dict, setuptools_cfg: dict
) -> None:
):
super().__init__(distribution)

@@ -424,0 +422,0 @@ self._project_cfg = project_cfg

@@ -18,11 +18,22 @@ """

from collections import defaultdict
from collections.abc import Iterable, Iterator
from functools import partial, wraps
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar, cast
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Generic,
Iterable,
Iterator,
List,
Tuple,
TypeVar,
cast,
)
from packaging.markers import default_environment as marker_env
from packaging.requirements import InvalidRequirement, Requirement
from packaging.specifiers import SpecifierSet
from packaging.version import InvalidVersion, Version
from .. import _static
from .._path import StrPath

@@ -40,3 +51,3 @@ from ..errors import FileError, OptionError

SingleCommandOptions: TypeAlias = dict[str, tuple[str, Any]]
SingleCommandOptions: TypeAlias = Dict[str, Tuple[str, Any]]
"""Dict that associate the name of the options of a particular command to a

@@ -47,3 +58,3 @@ tuple. The first element of the tuple indicates the origin of the option value

"""
AllCommandOptions: TypeAlias = dict[str, SingleCommandOptions]
AllCommandOptions: TypeAlias = Dict[str, SingleCommandOptions]
"""cmd name => its options"""

@@ -108,3 +119,3 @@ Target = TypeVar("Target", "Distribution", "DistributionMetadata")

# TODO: Temporary cast until mypy 1.12 is released with upstream fixes from typeshed
_Distribution.parse_config_files(dist, filenames=cast(list[str], filenames))
_Distribution.parse_config_files(dist, filenames=cast(List[str], filenames))
handlers = parse_configuration(

@@ -241,3 +252,3 @@ dist, dist.command_options, ignore_option_errors=ignore_option_errors

aliases: ClassVar[dict[str, str]] = {}
aliases: dict[str, str] = {}
"""Options aliases.

@@ -255,3 +266,3 @@ For compatibility with various packages. E.g.: d2to1 and pbr.

ensure_discovered: expand.EnsurePackagesDiscovered,
) -> None:
):
self.ignore_option_errors = ignore_option_errors

@@ -262,3 +273,3 @@ self.target_obj: Target = target_obj

self.ensure_discovered = ensure_discovered
self._referenced_files = set[str]()
self._referenced_files: set[str] = set()
"""After parsing configurations, this property will enumerate

@@ -273,3 +284,3 @@ all files referenced by the "file:" directive. Private API for setuptools only.

for full_name, value in options.items():
pre, _sep, name = full_name.partition(cls.section_prefix)
pre, sep, name = full_name.partition(cls.section_prefix)
if pre:

@@ -283,3 +294,3 @@ continue

raise NotImplementedError(
f'{self.__class__.__name__} must provide .parsers property'
'%s must provide .parsers property' % self.__class__.__name__
)

@@ -379,3 +390,3 @@

)
return _static.Str(value)
return value

@@ -403,3 +414,3 @@ return parser

if not value.startswith(include_directive):
return _static.Str(value)
return value

@@ -409,4 +420,3 @@ spec = value[len(include_directive) :]

self._referenced_files.update(filepaths)
# XXX: Is marking as static contents coming from files too optimistic?
return _static.Str(expand.read_files(filepaths, root_dir))
return expand.read_files(filepaths, root_dir)

@@ -425,3 +435,3 @@ def _parse_attr(self, value, package_dir, root_dir: StrPath):

if not value.startswith(attr_directive):
return _static.Str(value)
return value

@@ -482,3 +492,3 @@ attr_desc = value.replace(attr_directive, '')

def parse_section(self, section_options) -> None:
def parse_section(self, section_options):
"""Parses configuration file section.

@@ -558,3 +568,3 @@

root_dir: StrPath | None = os.curdir,
) -> None:
):
super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)

@@ -567,25 +577,19 @@ self.package_dir = package_dir

"""Metadata item name to parser function mapping."""
parse_list_static = self._get_parser_compound(self._parse_list, _static.List)
parse_dict_static = self._get_parser_compound(self._parse_dict, _static.Dict)
parse_list = self._parse_list
parse_file = partial(self._parse_file, root_dir=self.root_dir)
parse_dict = self._parse_dict
exclude_files_parser = self._exclude_files_parser
return {
'author': _static.Str,
'author_email': _static.Str,
'maintainer': _static.Str,
'maintainer_email': _static.Str,
'platforms': parse_list_static,
'keywords': parse_list_static,
'provides': parse_list_static,
'obsoletes': parse_list_static,
'classifiers': self._get_parser_compound(parse_file, parse_list_static),
'platforms': parse_list,
'keywords': parse_list,
'provides': parse_list,
'obsoletes': parse_list,
'classifiers': self._get_parser_compound(parse_file, parse_list),
'license': exclude_files_parser('license'),
'license_files': parse_list_static,
'license_files': parse_list,
'description': parse_file,
'long_description': parse_file,
'long_description_content_type': _static.Str,
'version': self._parse_version, # Cannot be marked as dynamic
'url': _static.Str,
'project_urls': parse_dict_static,
'version': self._parse_version,
'project_urls': parse_dict,
}

@@ -628,3 +632,3 @@

ensure_discovered: expand.EnsurePackagesDiscovered,
) -> None:
):
super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)

@@ -647,4 +651,3 @@ self.root_dir = target_obj.src_root

# will have stripped each line and filtered out empties.
return _static.List(line for line in parsed if not line.startswith("#"))
# ^-- Use `_static.List` to mark a non-`Dynamic` Core Metadata
return [line for line in parsed if not line.startswith("#")]

@@ -656,2 +659,3 @@ @property

parse_bool = self._parse_bool
parse_dict = self._parse_dict
parse_cmdclass = self._parse_cmdclass

@@ -662,3 +666,3 @@

'include_package_data': parse_bool,
'package_dir': self._parse_dict,
'package_dir': parse_dict,
'scripts': parse_list,

@@ -673,3 +677,3 @@ 'eager_resources': parse_list,

),
'install_requires': partial( # Core Metadata
'install_requires': partial(
self._parse_requirements_list, "install_requires"

@@ -681,3 +685,3 @@ ),

'py_modules': parse_list,
'python_requires': _static.SpecifierSet, # Core Metadata
'python_requires': SpecifierSet,
'cmdclass': parse_cmdclass,

@@ -733,3 +737,3 @@ }

def parse_section_entry_points(self, section_options) -> None:
def parse_section_entry_points(self, section_options):
"""Parses `entry_points` configuration file section.

@@ -746,3 +750,3 @@

def parse_section_package_data(self, section_options) -> None:
def parse_section_package_data(self, section_options):
"""Parses `package_data` configuration file section.

@@ -754,3 +758,3 @@

def parse_section_exclude_package_data(self, section_options) -> None:
def parse_section_exclude_package_data(self, section_options):
"""Parses `exclude_package_data` configuration file section.

@@ -762,3 +766,3 @@

def parse_section_extras_require(self, section_options) -> None: # Core Metadata
def parse_section_extras_require(self, section_options):
"""Parses `extras_require` configuration file section.

@@ -773,6 +777,5 @@

self['extras_require'] = _static.Dict(parsed)
# ^-- Use `_static.Dict` to mark a non-`Dynamic` Core Metadata
self['extras_require'] = parsed
def parse_section_data_files(self, section_options) -> None:
def parse_section_data_files(self, section_options):
"""Parses `data_files` configuration file section.

@@ -779,0 +782,0 @@

@@ -7,4 +7,2 @@ from __future__ import annotations

import sys
from types import CodeType
from typing import Any, Literal, TypeVar

@@ -16,4 +14,2 @@ from packaging.version import Version

_T = TypeVar("_T")
__all__ = ['Require', 'find_module']

@@ -33,3 +29,3 @@

format=None,
) -> None:
):
if format is None and requested_version is not None:

@@ -49,3 +45,3 @@ format = Version

if self.requested_version is not None:
return f'{self.name}-{self.requested_version}'
return '%s-%s' % (self.name, self.requested_version)
return self.name

@@ -62,5 +58,3 @@

def get_version(
self, paths=None, default: _T | Literal["unknown"] = "unknown"
) -> _T | Literal["unknown"] | None | Any:
def get_version(self, paths=None, default: str = "unknown"):
"""Get version number of installed module, 'None', or 'default'

@@ -78,3 +72,3 @@

try:
f, _p, _i = find_module(self.module, paths)
f, p, i = find_module(self.module, paths)
except ImportError:

@@ -121,5 +115,3 @@ return None

def get_module_constant(
module, symbol, default: _T | int = -1, paths=None
) -> _T | int | None | Any:
def get_module_constant(module, symbol, default: str | int = -1, paths=None):
"""Find 'module' by searching 'paths', and extract 'symbol'

@@ -132,3 +124,3 @@

try:
f, path, (_suffix, _mode, kind) = info = find_module(module, paths)
f, path, (suffix, mode, kind) = info = find_module(module, paths)
except ImportError:

@@ -153,5 +145,3 @@ # Module doesn't exist

def extract_constant(
code: CodeType, symbol: str, default: _T | int = -1
) -> _T | int | None | Any:
def extract_constant(code, symbol, default: str | int = -1):
"""Extract the constant value of 'symbol' from 'code'

@@ -185,3 +175,2 @@

if op == LOAD_CONST:
assert arg is not None
const = code.co_consts[arg]

@@ -188,0 +177,0 @@ elif arg == name_idx and (op == STORE_NAME or op == STORE_GLOBAL):

@@ -44,7 +44,7 @@ """Automatic discovery of Python modules and packages (for inclusion in the

import os
from collections.abc import Iterable, Iterator, Mapping
from collections.abc import Iterator
from fnmatch import fnmatchcase
from glob import glob
from pathlib import Path
from typing import TYPE_CHECKING, ClassVar
from typing import TYPE_CHECKING, Iterable, Mapping

@@ -75,3 +75,3 @@ import _distutils_hack.override # noqa: F401

def __init__(self, *patterns: str) -> None:
def __init__(self, *patterns: str):
self._patterns = dict.fromkeys(patterns)

@@ -89,4 +89,4 @@

ALWAYS_EXCLUDE: ClassVar[tuple[str, ...]] = ()
DEFAULT_EXCLUDE: ClassVar[tuple[str, ...]] = ()
ALWAYS_EXCLUDE: tuple[str, ...] = ()
DEFAULT_EXCLUDE: tuple[str, ...] = ()

@@ -306,3 +306,3 @@ @classmethod

def __init__(self, distribution: Distribution) -> None:
def __init__(self, distribution: Distribution):
self.dist = distribution

@@ -486,3 +486,3 @@ self._called = False

def analyse_name(self) -> None:
def analyse_name(self):
"""The packages/modules are the essential contribution of the author.

@@ -489,0 +489,0 @@ Therefore the name of the distribution can be derived from them.

from __future__ import annotations
import functools
import io

@@ -10,6 +9,15 @@ import itertools

import sys
from collections.abc import Iterable, Iterator, MutableMapping, Sequence
from glob import glob
from collections.abc import Iterable
from glob import iglob
from pathlib import Path
from typing import TYPE_CHECKING, Any, Union
from typing import (
TYPE_CHECKING,
Any,
Dict,
List,
MutableMapping,
Sequence,
Tuple,
Union,
)

@@ -21,15 +29,13 @@ from more_itertools import partition, unique_everseen

from setuptools._path import StrPath
from . import (
_entry_points,
_reqs,
_static,
command as _, # noqa: F401 # imported for side-effects
)
from ._importlib import metadata
from ._normalization import _canonicalize_license_expression
from ._path import StrPath
from ._reqs import _StrOrIter
from .config import pyprojecttoml, setupcfg
from .discovery import ConfigDiscovery
from .errors import InvalidConfigError
from .monkey import get_unpatched

@@ -51,3 +57,2 @@ from .warnings import InformationOnly, SetuptoolsDeprecationWarning

__all__ = ['Distribution']

@@ -65,6 +70,6 @@

"""
_Sequence: TypeAlias = Union[tuple[str, ...], list[str]]
_Sequence: TypeAlias = Union[Tuple[str, ...], List[str]]
# This is how stringifying _Sequence would look in Python 3.10
_sequence_type_repr = "tuple[str, ...] | list[str]"
_OrderedStrSequence: TypeAlias = Union[str, dict[str, Any], Sequence[str]]
_OrderedStrSequence: TypeAlias = Union[str, Dict[str, Any], Sequence[str]]
"""

@@ -94,3 +99,3 @@ :meta private:

raise DistutilsSetupError(
f"{attr!r} must be importable 'module:attrs' string (got {value!r})"
"%r must be importable 'module:attrs' string (got %r)" % (attr, value)
) from e

@@ -120,5 +125,6 @@

raise DistutilsSetupError(
f"Distribution contains no modules or packages for namespace package {nsp!r}"
"Distribution contains no modules or packages for "
+ "namespace package %r" % nsp
)
parent, _sep, _child = nsp.rpartition('.')
parent, sep, child = nsp.rpartition('.')
if parent and parent not in ns_packages:

@@ -155,3 +161,3 @@ distutils.log.warn(

def _check_extra(extra, reqs):
_name, _sep, marker = extra.partition(':')
name, sep, marker = extra.partition(':')
try:

@@ -221,4 +227,4 @@ _check_marker(marker)

raise DistutilsSetupError(
f"{attr!r} must be a dictionary mapping package names to lists of "
"string wildcard patterns"
"{!r} must be a dictionary mapping package names to lists of "
"string wildcard patterns".format(attr)
)

@@ -228,5 +234,5 @@ for k, v in value.items():

raise DistutilsSetupError(
f"keys of {attr!r} dict must be strings (got {k!r})"
"keys of {!r} dict must be strings (got {!r})".format(attr, k)
)
assert_string_list(dist, f'values of {attr!r} dict', v)
assert_string_list(dist, 'values of {!r} dict'.format(attr), v)

@@ -301,3 +307,2 @@

'provides_extras': dict, # behaves like an ordered set
'license_expression': lambda: None,
'license_file': lambda: None,

@@ -336,3 +341,3 @@ 'license_files': lambda: None,

# sdist (e.g. `version = file: VERSION.txt`)
self._referenced_files = set[str]()
self._referenced_files: set[str] = set()

@@ -408,56 +413,11 @@ self.set_defaults = ConfigDiscovery(self)

extras_require = getattr(self, "extras_require", None) or {}
self.install_requires = list(map(str, _reqs.parse(install_requires)))
self.extras_require = {
k: list(map(str, _reqs.parse(v or []))) for k, v in extras_require.items()
}
# Preserve the "static"-ness of values parsed from config files
list_ = _static.List if _static.is_static(install_requires) else list
self.install_requires = list_(map(str, _reqs.parse(install_requires)))
dict_ = _static.Dict if _static.is_static(extras_require) else dict
self.extras_require = dict_(
(k, list(map(str, _reqs.parse(v or [])))) for k, v in extras_require.items()
)
def _finalize_license_expression(self) -> None:
"""
Normalize license and license_expression.
>>> dist = Distribution({"license_expression": _static.Str("mit aNd gpl-3.0-OR-later")})
>>> _static.is_static(dist.metadata.license_expression)
True
>>> dist._finalize_license_expression()
>>> _static.is_static(dist.metadata.license_expression) # preserve "static-ness"
True
>>> print(dist.metadata.license_expression)
MIT AND GPL-3.0-or-later
"""
classifiers = self.metadata.get_classifiers()
license_classifiers = [cl for cl in classifiers if cl.startswith("License :: ")]
license_expr = self.metadata.license_expression
if license_expr:
str_ = _static.Str if _static.is_static(license_expr) else str
normalized = str_(_canonicalize_license_expression(license_expr))
if license_expr != normalized:
InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'")
self.metadata.license_expression = normalized
if license_classifiers:
raise InvalidConfigError(
"License classifiers have been superseded by license expressions "
"(see https://peps.python.org/pep-0639/). Please remove:\n\n"
+ "\n".join(license_classifiers),
)
elif license_classifiers:
pypa_guides = "guides/writing-pyproject-toml/#license"
SetuptoolsDeprecationWarning.emit(
"License classifiers are deprecated.",
"Please consider removing the following classifiers in favor of a "
"SPDX license expression:\n\n" + "\n".join(license_classifiers),
see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",
# Warning introduced on 2025-02-17
# TODO: Should we add a due date? It may affect old/unmaintained
# packages in the ecosystem and cause problems...
)
def _finalize_license_files(self) -> None:
"""Compute names of all license files which should be included."""
license_files: list[str] | None = self.metadata.license_files
patterns = license_files or []
patterns: list[str] = license_files if license_files else []

@@ -473,86 +433,22 @@ license_file: str | None = self.metadata.license_file

patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']
files = self._expand_patterns(patterns, enforce_match=False)
else: # Patterns explicitly given by the user
files = self._expand_patterns(patterns, enforce_match=True)
self.metadata.license_files = list(unique_everseen(files))
self.metadata.license_files = list(
unique_everseen(self._expand_patterns(patterns))
)
@classmethod
def _expand_patterns(
cls, patterns: list[str], enforce_match: bool = True
) -> Iterator[str]:
@staticmethod
def _expand_patterns(patterns):
"""
>>> getfixture('sample_project_cwd')
>>> list(Distribution._expand_patterns(['LICENSE.txt']))
['LICENSE.txt']
>>> list(Distribution._expand_patterns(['LICENSE']))
['LICENSE']
>>> list(Distribution._expand_patterns(['pyproject.toml', 'LIC*']))
['pyproject.toml', 'LICENSE.txt']
>>> list(Distribution._expand_patterns(['src/**/*.dat']))
['src/sample/package_data.dat']
['pyproject.toml', 'LICENSE']
"""
return (
path.replace(os.sep, "/")
path
for pattern in patterns
for path in sorted(cls._find_pattern(pattern, enforce_match))
for path in sorted(iglob(pattern))
if not path.endswith('~') and os.path.isfile(path)
)
@staticmethod
def _find_pattern(pattern: str, enforce_match: bool = True) -> list[str]:
r"""
>>> getfixture('sample_project_cwd')
>>> Distribution._find_pattern("LICENSE.txt")
['LICENSE.txt']
>>> Distribution._find_pattern("/LICENSE.MIT")
Traceback (most recent call last):
...
setuptools.errors.InvalidConfigError: Pattern '/LICENSE.MIT' should be relative...
>>> Distribution._find_pattern("../LICENSE.MIT")
Traceback (most recent call last):
...
setuptools.warnings.SetuptoolsDeprecationWarning: ...Pattern '../LICENSE.MIT' cannot contain '..'...
>>> Distribution._find_pattern("LICEN{CSE*")
Traceback (most recent call last):
...
setuptools.warnings.SetuptoolsDeprecationWarning: ...Pattern 'LICEN{CSE*' contains invalid characters...
"""
pypa_guides = "specifications/glob-patterns/"
if ".." in pattern:
SetuptoolsDeprecationWarning.emit(
f"Pattern {pattern!r} cannot contain '..'",
"""
Please ensure the files specified are contained by the root
of the Python package (normally marked by `pyproject.toml`).
""",
see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",
due_date=(2026, 3, 20), # Introduced in 2025-03-20
# Replace with InvalidConfigError after deprecation
)
if pattern.startswith((os.sep, "/")) or ":\\" in pattern:
raise InvalidConfigError(
f"Pattern {pattern!r} should be relative and must not start with '/'"
)
if re.match(r'^[\w\-\.\/\*\?\[\]]+$', pattern) is None:
SetuptoolsDeprecationWarning.emit(
"Please provide a valid glob pattern.",
"Pattern {pattern!r} contains invalid characters.",
pattern=pattern,
see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",
due_date=(2026, 3, 20), # Introduced in 2025-02-20
)
found = glob(pattern, recursive=True)
if enforce_match and not found:
SetuptoolsDeprecationWarning.emit(
"Cannot find any files for the given pattern.",
"Pattern {pattern!r} did not match any files.",
pattern=pattern,
due_date=(2026, 3, 20), # Introduced in 2025-02-20
# PEP 639 requires us to error, but as a transition period
# we will only issue a warning to give people time to prepare.
# After the transition, this should raise an InvalidConfigError.
)
return found
# FIXME: 'Distribution._parse_config_files' is too complex (14)

@@ -612,4 +508,4 @@ def _parse_config_files(self, filenames=None): # noqa: C901

val = parser.get(section, opt)
opt = self._enforce_underscore(opt, section)
opt = self._enforce_option_lowercase(opt, section)
opt = self.warn_dash_deprecation(opt, section)
opt = self.make_option_lowercase(opt, section)
opt_dict[opt] = (filename, val)

@@ -639,38 +535,57 @@

def _enforce_underscore(self, opt: str, section: str) -> str:
if "-" not in opt or self._skip_setupcfg_normalization(section):
def warn_dash_deprecation(self, opt: str, section: str):
if section in (
'options.extras_require',
'options.data_files',
):
return opt
underscore_opt = opt.replace('-', '_')
affected = f"(Affected: {self.metadata.name})." if self.metadata.name else ""
SetuptoolsDeprecationWarning.emit(
f"Invalid dash-separated key {opt!r} in {section!r} (setup.cfg), "
f"please use the underscore name {underscore_opt!r} instead.",
f"""
Usage of dash-separated {opt!r} will not be supported in future
versions. Please use the underscore name {underscore_opt!r} instead.
{affected}
""",
see_docs="userguide/declarative_config.html",
due_date=(2026, 3, 3),
# Warning initially introduced in 3 Mar 2021
commands = list(
itertools.chain(
distutils.command.__all__,
self._setuptools_commands(),
)
)
if (
not section.startswith('options')
and section != 'metadata'
and section not in commands
):
return underscore_opt
if '-' in opt:
SetuptoolsDeprecationWarning.emit(
"Invalid dash-separated options",
f"""
Usage of dash-separated {opt!r} will not be supported in future
versions. Please use the underscore name {underscore_opt!r} instead.
""",
see_docs="userguide/declarative_config.html",
due_date=(2025, 3, 3),
# Warning initially introduced in 3 Mar 2021
)
return underscore_opt
def _enforce_option_lowercase(self, opt: str, section: str) -> str:
if opt.islower() or self._skip_setupcfg_normalization(section):
def _setuptools_commands(self):
try:
entry_points = metadata.distribution('setuptools').entry_points
return {ep.name for ep in entry_points} # Avoid newer API for compatibility
except metadata.PackageNotFoundError:
# during bootstrapping, distribution doesn't exist
return []
def make_option_lowercase(self, opt: str, section: str):
if section != 'metadata' or opt.islower():
return opt
lowercase_opt = opt.lower()
affected = f"(Affected: {self.metadata.name})." if self.metadata.name else ""
SetuptoolsDeprecationWarning.emit(
f"Invalid uppercase key {opt!r} in {section!r} (setup.cfg), "
f"please use lowercase {lowercase_opt!r} instead.",
"Invalid uppercase configuration",
f"""
Usage of uppercase key {opt!r} in {section!r} will not be supported in
future versions. Please use lowercase {lowercase_opt!r} instead.
{affected}
""",
see_docs="userguide/declarative_config.html",
due_date=(2026, 3, 3),
due_date=(2025, 3, 3),
# Warning initially introduced in 6 Mar 2021

@@ -680,19 +595,2 @@ )

def _skip_setupcfg_normalization(self, section: str) -> bool:
skip = (
'options.extras_require',
'options.data_files',
'options.entry_points',
'options.package_data',
'options.exclude_package_data',
)
return section in skip or not self._is_setuptools_section(section)
def _is_setuptools_section(self, section: str) -> bool:
return (
section == "metadata"
or section.startswith("options")
or section in _setuptools_commands()
)
# FIXME: 'Distribution._set_command_options' is too complex (14)

@@ -716,6 +614,6 @@ def _set_command_options(self, command_obj, option_dict=None): # noqa: C901

if DEBUG:
self.announce(f" setting options for '{command_name}' command:")
self.announce(" setting options for '%s' command:" % command_name)
for option, (source, value) in option_dict.items():
if DEBUG:
self.announce(f" {option} = {value} (from {source})")
self.announce(" %s = %s (from %s)" % (option, value, source))
try:

@@ -740,3 +638,4 @@ bool_opts = [translate_longopt(o) for o in command_obj.boolean_options]

raise DistutilsOptionError(
f"error in {source}: command '{command_name}' has no such option '{option}'"
"error in %s: command '%s' has no such option '%s'"
% (source, command_name, option)
)

@@ -762,3 +661,3 @@ except ValueError as e:

ignore_option_errors: bool = False,
) -> None:
):
"""Parses configuration files from various levels

@@ -778,6 +677,5 @@ and loads configuration.

self._finalize_requires()
self._finalize_license_expression()
self._finalize_license_files()
def fetch_build_eggs(self, requires: _StrOrIter) -> list[metadata.Distribution]:
def fetch_build_eggs(self, requires: _StrOrIter):
"""Resolve pre-setup requirements"""

@@ -788,3 +686,3 @@ from .installer import _fetch_build_eggs

def finalize_options(self) -> None:
def finalize_options(self):
"""

@@ -854,3 +752,3 @@ Allow plugins to apply arbitrary operations to the

def get_command_class(self, command: str) -> type[distutils.cmd.Command]: # type: ignore[override] # Not doing complex overrides yet
def get_command_class(self, command: str):
"""Pluggable version of get_command_class()"""

@@ -887,3 +785,3 @@ if command in self.cmdclass:

def include(self, **attrs) -> None:
def include(self, **attrs):
"""Add items to distribution that are named in keyword arguments

@@ -910,3 +808,3 @@

def exclude_package(self, package: str) -> None:
def exclude_package(self, package: str):
"""Remove packages, modules, and extensions in named package"""

@@ -932,3 +830,3 @@

def has_contents_for(self, package: str) -> bool:
def has_contents_for(self, package: str):
"""Return true if 'exclude_package(package)' would do something"""

@@ -953,3 +851,3 @@

except AttributeError as e:
raise DistutilsSetupError(f"{name}: No such distribution setting") from e
raise DistutilsSetupError("%s: No such distribution setting" % name) from e
if old is not None and not isinstance(old, _sequence):

@@ -972,3 +870,3 @@ raise DistutilsSetupError(

except AttributeError as e:
raise DistutilsSetupError(f"{name}: No such distribution setting") from e
raise DistutilsSetupError("%s: No such distribution setting" % name) from e
if old is None:

@@ -984,3 +882,3 @@ setattr(self, name, value)

def exclude(self, **attrs) -> None:
def exclude(self, **attrs):
"""Remove items from distribution that are named in keyword arguments

@@ -1024,3 +922,3 @@

while command in aliases:
_src, alias = aliases[command]
src, alias = aliases[command]
del aliases[command] # ensure each alias can expand only once!

@@ -1043,3 +941,3 @@ import shlex

def get_cmdline_options(self) -> dict[str, dict[str, str | None]]:
def get_cmdline_options(self):
"""Return a '{cmd: {opt:val}}' map of all command-line options

@@ -1054,6 +952,5 @@

d: dict[str, dict[str, str | None]] = {}
d = {}
for cmd, opts in self.command_options.items():
val: str | None
for opt, (src, val) in opts.items():

@@ -1093,3 +990,3 @@ if src != "command line":

if isinstance(ext, tuple):
name, _buildinfo = ext
name, buildinfo = ext
else:

@@ -1129,3 +1026,3 @@ name = ext.name

def run_command(self, command) -> None:
def run_command(self, command):
self.set_defaults()

@@ -1138,16 +1035,4 @@ # Postpone defaults until all explicit configuration is considered

@functools.cache
def _setuptools_commands() -> set[str]:
try:
# Use older API for importlib.metadata compatibility
entry_points = metadata.distribution('setuptools').entry_points
eps: Iterable[str] = (ep.name for ep in entry_points)
except metadata.PackageNotFoundError:
# during bootstrapping, distribution doesn't exist
eps = []
return {*distutils.command.__all__, *eps}
class DistDeprecationWarning(SetuptoolsDeprecationWarning):
"""Class for warning about deprecations in dist in
setuptools. Not ignored by default, unlike DeprecationWarning."""

@@ -150,3 +150,3 @@ from __future__ import annotations

**kw,
) -> None:
):
# The *args is needed for compatibility as calls may use positional

@@ -153,0 +153,0 @@ # arguments. py_limited_api may be set only via keyword.

@@ -9,17 +9,10 @@ """

from __future__ import annotations
import fnmatch
import os
import re
from collections.abc import Iterable, Iterator
from typing import TYPE_CHECKING, AnyStr, overload
if TYPE_CHECKING:
from _typeshed import BytesPath, StrOrBytesPath, StrPath
__all__ = ["glob", "iglob", "escape"]
def glob(pathname: AnyStr, recursive: bool = False) -> list[AnyStr]:
def glob(pathname, recursive: bool = False):
"""Return a list of paths matching a pathname pattern.

@@ -38,3 +31,3 @@

def iglob(pathname: AnyStr, recursive: bool = False) -> Iterator[AnyStr]:
def iglob(pathname, recursive: bool = False):
"""Return an iterator which yields the paths matching a pathname pattern.

@@ -57,3 +50,3 @@

def _iglob(pathname: AnyStr, recursive: bool) -> Iterator[AnyStr]:
def _iglob(pathname, recursive):
dirname, basename = os.path.split(pathname)

@@ -79,3 +72,3 @@ glob_in_dir = glob2 if recursive and _isrecursive(basename) else glob1

if dirname != pathname and has_magic(dirname):
dirs: Iterable[AnyStr] = _iglob(dirname, recursive)
dirs = _iglob(dirname, recursive)
else:

@@ -95,7 +88,3 @@ dirs = [dirname]

@overload
def glob1(dirname: StrPath, pattern: str) -> list[str]: ...
@overload
def glob1(dirname: BytesPath, pattern: bytes) -> list[bytes]: ...
def glob1(dirname: StrOrBytesPath, pattern: str | bytes) -> list[str] | list[bytes]:
def glob1(dirname, pattern):
if not dirname:

@@ -110,4 +99,3 @@ if isinstance(pattern, bytes):

return []
# mypy false-positives: str or bytes type possibility is always kept in sync
return fnmatch.filter(names, pattern) # type: ignore[type-var, return-value]
return fnmatch.filter(names, pattern)

@@ -131,7 +119,3 @@

@overload
def glob2(dirname: StrPath, pattern: str) -> Iterator[str]: ...
@overload
def glob2(dirname: BytesPath, pattern: bytes) -> Iterator[bytes]: ...
def glob2(dirname: StrOrBytesPath, pattern: str | bytes) -> Iterator[str | bytes]:
def glob2(dirname, pattern):
assert _isrecursive(pattern)

@@ -143,7 +127,3 @@ yield pattern[:0]

# Recursively yields relative pathnames inside a literal directory.
@overload
def _rlistdir(dirname: StrPath) -> Iterator[str]: ...
@overload
def _rlistdir(dirname: BytesPath) -> Iterator[bytes]: ...
def _rlistdir(dirname: StrOrBytesPath) -> Iterator[str | bytes]:
def _rlistdir(dirname):
if not dirname:

@@ -160,6 +140,5 @@ if isinstance(dirname, bytes):

yield x
# mypy false-positives: str or bytes type possibility is always kept in sync
path = os.path.join(dirname, x) if dirname else x # type: ignore[arg-type]
path = os.path.join(dirname, x) if dirname else x
for y in _rlistdir(path):
yield os.path.join(x, y) # type: ignore[arg-type]
yield os.path.join(x, y)

@@ -171,10 +150,11 @@

def has_magic(s: str | bytes) -> bool:
def has_magic(s):
if isinstance(s, bytes):
return magic_check_bytes.search(s) is not None
match = magic_check_bytes.search(s)
else:
return magic_check.search(s) is not None
match = magic_check.search(s)
return match is not None
def _isrecursive(pattern: str | bytes) -> bool:
def _isrecursive(pattern):
if isinstance(pattern, bytes):

@@ -181,0 +161,0 @@ return pattern == b'**'

@@ -1,5 +0,2 @@

from __future__ import annotations
import glob
import itertools
import os

@@ -9,8 +6,6 @@ import subprocess

import tempfile
from functools import partial
import packaging.requirements
import packaging.utils
from . import _reqs
from ._importlib import metadata
from ._reqs import _StrOrIter
from .warnings import SetuptoolsDeprecationWarning

@@ -40,34 +35,21 @@ from .wheel import Wheel

def _present(req):
return any(_dist_matches_req(dist, req) for dist in metadata.distributions())
def _fetch_build_eggs(dist, requires: _StrOrIter):
import pkg_resources # Delay import to avoid unnecessary side-effects
def _fetch_build_eggs(dist, requires: _reqs._StrOrIter) -> list[metadata.Distribution]:
_DeprecatedInstaller.emit(stacklevel=3)
_warn_wheel_not_available(dist)
parsed_reqs = _reqs.parse(requires)
missing_reqs = itertools.filterfalse(_present, parsed_reqs)
needed_reqs = (
req for req in missing_reqs if not req.marker or req.marker.evaluate()
resolved_dists = pkg_resources.working_set.resolve(
_reqs.parse(requires, pkg_resources.Requirement), # required for compatibility
installer=partial(_fetch_build_egg_no_warn, dist), # avoid warning twice
replace_conflicting=True,
)
resolved_dists = [_fetch_build_egg_no_warn(dist, req) for req in needed_reqs]
for dist in resolved_dists:
# dist.locate_file('') is the directory containing EGG-INFO, where the importabl
# contents can be found.
sys.path.insert(0, str(dist.locate_file('')))
pkg_resources.working_set.add(dist, replace=True)
return resolved_dists
def _dist_matches_req(egg_dist, req):
return (
packaging.utils.canonicalize_name(egg_dist.name)
== packaging.utils.canonicalize_name(req.name)
and egg_dist.version in req.specifier
)
def _fetch_build_egg_no_warn(dist, req): # noqa: C901 # is too complex (16) # FIXME
import pkg_resources # Delay import to avoid unnecessary side-effects
def _fetch_build_egg_no_warn(dist, req): # noqa: C901 # is too complex (16) # FIXME
# Ignore environment markers; if supplied, it is required.

@@ -97,5 +79,5 @@ req = strip_marker(req)

eggs_dir = os.path.realpath(dist.get_egg_cache_dir())
cached_dists = metadata.Distribution.discover(path=glob.glob(f'{eggs_dir}/*.egg'))
for egg_dist in cached_dists:
if _dist_matches_req(egg_dist, req):
environment = pkg_resources.Environment()
for egg_dist in pkg_resources.find_distributions(eggs_dir):
if egg_dist in req and environment.can_add(egg_dist):
return egg_dist

@@ -130,3 +112,8 @@ with tempfile.TemporaryDirectory() as tmpdir:

wheel.install_as_egg(dist_location)
return metadata.Distribution.at(dist_location + '/EGG-INFO')
dist_metadata = pkg_resources.PathMetadata(
dist_location, os.path.join(dist_location, 'EGG-INFO')
)
return pkg_resources.Distribution.from_filename(
dist_location, metadata=dist_metadata
)

@@ -140,4 +127,6 @@

"""
import pkg_resources # Delay import to avoid unnecessary side-effects
# create a copy to avoid mutating the input
req = packaging.requirements.Requirement(str(req))
req = pkg_resources.Requirement.parse(str(req))
req.marker = None

@@ -148,5 +137,7 @@ return req

def _warn_wheel_not_available(dist):
import pkg_resources # Delay import to avoid unnecessary side-effects
try:
metadata.distribution('wheel')
except metadata.PackageNotFoundError:
pkg_resources.get_distribution('wheel')
except pkg_resources.DistributionNotFound:
dist.announce('WARNING: The wheel package is not available.', log.WARN)

@@ -161,2 +152,2 @@

"""
_DUE_DATE = 2025, 10, 31
# _DUE_DATE not decided yet

@@ -13,3 +13,3 @@ """

def run() -> None:
def run():
"""

@@ -16,0 +16,0 @@ Run the script in sys.argv[1] as if it had

@@ -14,3 +14,3 @@ import inspect

def configure() -> None:
def configure():
"""

@@ -39,4 +39,4 @@ Configure logging to emit warning and above to stderr

def set_threshold(level: int) -> int:
def set_threshold(level: int):
logging.root.setLevel(level * 10)
return set_threshold.unpatched(level)

@@ -11,3 +11,3 @@ """

import types
from typing import TypeVar, cast, overload
from typing import Type, TypeVar, cast, overload

@@ -62,3 +62,3 @@ import distutils.filelist

external_bases = (
cast(type[_T], cls)
cast(Type[_T], cls)
for cls in _get_mro(cls)

@@ -69,3 +69,3 @@ if not cls.__module__.startswith('setuptools')

if not base.__module__.startswith('distutils'):
msg = f"distutils has already been patched by {cls!r}"
msg = "distutils has already been patched by %r" % cls
raise AssertionError(msg)

@@ -79,3 +79,3 @@ return base

# we can't patch distutils.cmd, alas
distutils.core.Command = setuptools.Command # type: ignore[misc,assignment] # monkeypatching
distutils.core.Command = setuptools.Command

@@ -89,4 +89,4 @@ _patch_distribution_metadata()

# Install the patched Extension
distutils.core.Extension = setuptools.extension.Extension # type: ignore[misc,assignment] # monkeypatching
distutils.extension.Extension = setuptools.extension.Extension # type: ignore[misc,assignment] # monkeypatching
distutils.core.Extension = setuptools.extension.Extension
distutils.extension.Extension = setuptools.extension.Extension
if 'distutils.command.build_ext' in sys.modules:

@@ -93,0 +93,0 @@ sys.modules[

@@ -16,3 +16,3 @@ """

import platform
from typing import TYPE_CHECKING, TypedDict
from typing import TYPE_CHECKING

@@ -23,5 +23,2 @@ from more_itertools import unique_everseen

if TYPE_CHECKING:
from typing_extensions import LiteralString, NotRequired
# https://github.com/python/mypy/issues/8166

@@ -55,3 +52,3 @@ if not TYPE_CHECKING and platform.system() == 'Windows':

def __init__(self, arch) -> None:
def __init__(self, arch):
self.arch = arch.lower().replace('x64', 'amd64')

@@ -93,3 +90,3 @@

def current_dir(self, hidex86=False, x64=False) -> str:
def current_dir(self, hidex86=False, x64=False):
"""

@@ -115,6 +112,6 @@ Current platform specific subfolder.

if (self.current_cpu == 'amd64' and x64)
else rf'\{self.current_cpu}'
else r'\%s' % self.current_cpu
)
def target_dir(self, hidex86=False, x64=False) -> str:
def target_dir(self, hidex86=False, x64=False):
r"""

@@ -140,3 +137,3 @@ Target platform specific subfolder.

if (self.target_cpu == 'amd64' and x64)
else rf'\{self.target_cpu}'
else r'\%s' % self.target_cpu
)

@@ -164,3 +161,3 @@

if self.target_cpu == current
else self.target_dir().replace('\\', f'\\{current}_')
else self.target_dir().replace('\\', '\\%s_' % current)
)

@@ -186,7 +183,7 @@

def __init__(self, platform_info) -> None:
def __init__(self, platform_info):
self.pi = platform_info
@property
def visualstudio(self) -> str:
def visualstudio(self):
"""

@@ -239,3 +236,3 @@ Microsoft Visual Studio root registry key.

@property
def vc_for_python(self) -> str:
def vc_for_python(self):
"""

@@ -252,3 +249,3 @@ Microsoft Visual C++ for Python registry key.

@property
def microsoft_sdk(self) -> str:
def microsoft_sdk(self):
"""

@@ -289,3 +286,3 @@ Microsoft SDK registry key.

@property
def windows_kits_roots(self) -> str:
def windows_kits_roots(self):
"""

@@ -380,3 +377,3 @@ Microsoft Windows Kits Roots registry key.

def __init__(self, registry_info, vc_ver=None) -> None:
def __init__(self, registry_info, vc_ver=None):
self.ri = registry_info

@@ -441,3 +438,3 @@ self.pi = self.ri.pi

def find_programdata_vs_vers(self) -> dict[float, str]:
def find_programdata_vs_vers(self):
r"""

@@ -452,3 +449,3 @@ Find Visual studio 2017+ versions from information in

"""
vs_versions: dict[float, str] = {}
vs_versions = {}
instances_dir = r'C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances'

@@ -514,7 +511,7 @@

default = os.path.join(
self.ProgramFilesx86, f'Microsoft Visual Studio {self.vs_ver:0.1f}'
self.ProgramFilesx86, 'Microsoft Visual Studio %0.1f' % self.vs_ver
)
# Try to get path from registry, if fail use default path
return self.ri.lookup(self.ri.vs, f'{self.vs_ver:0.1f}') or default
return self.ri.lookup(self.ri.vs, '%0.1f' % self.vs_ver) or default

@@ -579,8 +576,7 @@ @property

default = os.path.join(
self.ProgramFilesx86,
rf'Microsoft Visual Studio {self.vs_ver:0.1f}\VC',
self.ProgramFilesx86, r'Microsoft Visual Studio %0.1f\VC' % self.vs_ver
)
# Try to get "VC++ for Python" path from registry as default path
reg_path = os.path.join(self.ri.vc_for_python, f'{self.vs_ver:0.1f}')
reg_path = os.path.join(self.ri.vc_for_python, '%0.1f' % self.vs_ver)
python_vc = self.ri.lookup(reg_path, 'installdir')

@@ -590,6 +586,6 @@ default_vc = os.path.join(python_vc, 'VC') if python_vc else default

# Try to get path from registry, if fail use default path
return self.ri.lookup(self.ri.vc, f'{self.vs_ver:0.1f}') or default_vc
return self.ri.lookup(self.ri.vc, '%0.1f' % self.vs_ver) or default_vc
@property
def WindowsSdkVersion(self) -> tuple[LiteralString, ...]:
def WindowsSdkVersion(self):
"""

@@ -613,3 +609,3 @@ Microsoft Windows SDK versions for specified MSVC++ version.

return '10.0', '8.1'
return ()
return None

@@ -629,3 +625,3 @@ @property

@property
def WindowsSdkDir(self) -> str | None: # noqa: C901 # is too complex (12) # FIXME
def WindowsSdkDir(self): # noqa: C901 # is too complex (12) # FIXME
"""

@@ -639,6 +635,6 @@ Microsoft Windows SDK directory.

"""
sdkdir: str | None = ''
sdkdir = ''
for ver in self.WindowsSdkVersion:
# Try to get it from registry
loc = os.path.join(self.ri.windows_sdk, f'v{ver}')
loc = os.path.join(self.ri.windows_sdk, 'v%s' % ver)
sdkdir = self.ri.lookup(loc, 'installationfolder')

@@ -649,3 +645,3 @@ if sdkdir:

# Try to get "VC++ for Python" version from registry
path = os.path.join(self.ri.vc_for_python, f'{self.vc_ver:0.1f}')
path = os.path.join(self.ri.vc_for_python, '%0.1f' % self.vc_ver)
install_base = self.ri.lookup(path, 'installdir')

@@ -658,3 +654,3 @@ if install_base:

intver = ver[: ver.rfind('.')]
path = rf'Microsoft SDKs\Windows Kits\{intver}'
path = r'Microsoft SDKs\Windows Kits\%s' % intver
d = os.path.join(self.ProgramFiles, path)

@@ -666,3 +662,3 @@ if os.path.isdir(d):

for ver in self.WindowsSdkVersion:
path = rf'Microsoft SDKs\Windows\v{ver}'
path = r'Microsoft SDKs\Windows\v%s' % ver
d = os.path.join(self.ProgramFiles, path)

@@ -693,4 +689,4 @@ if os.path.isdir(d):

hidex86 = True if self.vs_ver <= 12.0 else False
arch = self.pi.current_dir(x64=True, hidex86=hidex86).replace('\\', '-')
fx = f'WinSDK-NetFx{netfxver}Tools{arch}'
arch = self.pi.current_dir(x64=True, hidex86=hidex86)
fx = 'WinSDK-NetFx%dTools%s' % (netfxver, arch.replace('\\', '-'))

@@ -704,3 +700,3 @@ # list all possibles registry paths

for ver in self.WindowsSdkVersion:
regpaths += [os.path.join(self.ri.windows_sdk, f'v{ver}A', fx)]
regpaths += [os.path.join(self.ri.windows_sdk, 'v%sA' % ver, fx)]

@@ -725,3 +721,3 @@ # Return installation folder from the more recent path

"""
path = os.path.join(self.ri.visualstudio, rf'{self.vs_ver:0.1f}\Setup\F#')
path = os.path.join(self.ri.visualstudio, r'%0.1f\Setup\F#' % self.vs_ver)
return self.ri.lookup(path, 'productdir') or ''

@@ -744,3 +740,3 @@

for ver in vers:
sdkdir = self.ri.lookup(self.ri.windows_kits_roots, f'kitsroot{ver}')
sdkdir = self.ri.lookup(self.ri.windows_kits_roots, 'kitsroot%s' % ver)
if sdkdir:

@@ -831,3 +827,3 @@ return sdkdir or ''

@property
def FrameworkVersion32(self) -> tuple[str, ...]:
def FrameworkVersion32(self):
"""

@@ -844,3 +840,3 @@ Microsoft .NET Framework 32bit versions.

@property
def FrameworkVersion64(self) -> tuple[str, ...]:
def FrameworkVersion64(self):
"""

@@ -856,3 +852,3 @@ Microsoft .NET Framework 64bit versions.

def _find_dot_net_versions(self, bits) -> tuple[str, ...]:
def _find_dot_net_versions(self, bits):
"""

@@ -872,4 +868,4 @@ Find Microsoft .NET Framework versions.

# Find actual .NET version in registry
reg_ver = self.ri.lookup(self.ri.vc, f'frameworkver{bits}')
dot_net_dir = getattr(self, f'FrameworkDir{bits}')
reg_ver = self.ri.lookup(self.ri.vc, 'frameworkver%d' % bits)
dot_net_dir = getattr(self, 'FrameworkDir%d' % bits)
ver = reg_ver or self._use_last_dir_name(dot_net_dir, 'v') or ''

@@ -886,3 +882,3 @@

return 'v3.0', 'v2.0.50727'
return ()
return None

@@ -915,10 +911,2 @@ @staticmethod

class _EnvironmentDict(TypedDict):
include: str
lib: str
libpath: str
path: str
py_vcruntime_redist: NotRequired[str | None]
class EnvironmentInfo:

@@ -948,3 +936,3 @@ """

def __init__(self, arch, vc_ver=None, vc_min_ver=0) -> None:
def __init__(self, arch, vc_ver=None, vc_min_ver=0):
self.pi = PlatformInfo(arch)

@@ -998,3 +986,3 @@ self.ri = RegistryInfo(self.pi)

paths += [r'Team Tools\Performance Tools']
paths += [rf'Team Tools\Performance Tools{arch_subdir}']
paths += [r'Team Tools\Performance Tools%s' % arch_subdir]

@@ -1032,6 +1020,6 @@ return [os.path.join(self.si.VSInstallDir, path) for path in paths]

arch_subdir = self.pi.target_dir(hidex86=True)
paths = [f'Lib{arch_subdir}', rf'ATLMFC\Lib{arch_subdir}']
paths = ['Lib%s' % arch_subdir, r'ATLMFC\Lib%s' % arch_subdir]
if self.vs_ver >= 14.0:
paths += [rf'Lib\store{arch_subdir}']
paths += [r'Lib\store%s' % arch_subdir]

@@ -1063,12 +1051,2 @@ return [os.path.join(self.si.VCInstallDir, path) for path in paths]

paths
When host CPU is ARM, the tools should be found for ARM.
>>> getfixture('windows_only')
>>> mp = getfixture('monkeypatch')
>>> mp.setattr(PlatformInfo, 'current_cpu', 'arm64')
>>> ei = EnvironmentInfo(arch='irrelevant')
>>> paths = ei.VCTools
>>> any('HostARM64' in path for path in paths)
True
"""

@@ -1081,11 +1059,12 @@ si = self.si

if arch_subdir:
tools += [os.path.join(si.VCInstallDir, f'Bin{arch_subdir}')]
tools += [os.path.join(si.VCInstallDir, 'Bin%s' % arch_subdir)]
if self.vs_ver == 14.0:
path = f'Bin{self.pi.current_dir(hidex86=True)}'
path = 'Bin%s' % self.pi.current_dir(hidex86=True)
tools += [os.path.join(si.VCInstallDir, path)]
elif self.vs_ver >= 15.0:
host_id = self.pi.current_cpu.replace('amd64', 'x64').upper()
host_dir = os.path.join('bin', f'Host{host_id}%s')
host_dir = (
r'bin\HostX86%s' if self.pi.current_is_x86() else r'bin\HostX64%s'
)
tools += [

@@ -1119,3 +1098,3 @@ os.path.join(si.VCInstallDir, host_dir % self.pi.target_dir(x64=True))

arch_subdir = self.pi.target_dir(hidex86=True, x64=True)
return [os.path.join(self.si.WindowsSdkDir, f'Lib{arch_subdir}')]
return [os.path.join(self.si.WindowsSdkDir, 'Lib%s' % arch_subdir)]

@@ -1126,3 +1105,3 @@ else:

libver = self._sdk_subdir
return [os.path.join(lib, f'{libver}um{arch_subdir}')]
return [os.path.join(lib, '%sum%s' % (libver, arch_subdir))]

@@ -1150,5 +1129,5 @@ @property

return [
os.path.join(include, f'{sdkver}shared'),
os.path.join(include, f'{sdkver}um'),
os.path.join(include, f'{sdkver}winrt'),
os.path.join(include, '%sshared' % sdkver),
os.path.join(include, '%sum' % sdkver),
os.path.join(include, '%swinrt' % sdkver),
]

@@ -1188,3 +1167,3 @@

'Microsoft.VCLibs',
f'{self.vs_ver:0.1f}',
'%0.1f' % self.vs_ver,
'References',

@@ -1224,3 +1203,3 @@ 'CommonConfiguration',

arch_subdir = self.pi.current_dir(x64=True)
path = f'Bin{arch_subdir}'
path = 'Bin%s' % arch_subdir
yield os.path.join(self.si.WindowsSdkDir, path)

@@ -1233,3 +1212,3 @@

arch_subdir = self.pi.current_dir(hidex86=True, x64=True)
path = rf'Bin\NETFX 4.0 Tools{arch_subdir}'
path = r'Bin\NETFX 4.0 Tools%s' % arch_subdir
yield os.path.join(self.si.WindowsSdkDir, path)

@@ -1241,3 +1220,3 @@

sdkver = self.si.WindowsSdkLastVersion
yield os.path.join(path, f'{sdkver}{arch_subdir}')
yield os.path.join(path, '%s%s' % (sdkver, arch_subdir))

@@ -1258,3 +1237,3 @@ if self.si.WindowsSDKExecutablePath:

ucrtver = self.si.WindowsSdkLastVersion
return (f'{ucrtver}\\') if ucrtver else ''
return ('%s\\' % ucrtver) if ucrtver else ''

@@ -1321,3 +1300,3 @@ @property

arch_subdir = self.pi.target_dir(x64=True)
return [os.path.join(self.si.NetFxSdkDir, rf'lib\um{arch_subdir}')]
return [os.path.join(self.si.NetFxSdkDir, r'lib\um%s' % arch_subdir)]

@@ -1370,3 +1349,3 @@ @property

path = rf'MSBuild\{self.vs_ver:0.1f}\bin{arch_subdir}'
path = r'MSBuild\%0.1f\bin%s' % (self.vs_ver, arch_subdir)
build = [os.path.join(base_path, path)]

@@ -1411,3 +1390,3 @@

ucrtver = self._ucrt_subdir
return [os.path.join(lib, f'{ucrtver}ucrt{arch_subdir}')]
return [os.path.join(lib, '%sucrt%s' % (ucrtver, arch_subdir))]

@@ -1428,3 +1407,3 @@ @property

include = os.path.join(self.si.UniversalCRTSdkDir, 'include')
return [os.path.join(include, f'{self._ucrt_subdir}ucrt')]
return [os.path.join(include, '%sucrt' % self._ucrt_subdir)]

@@ -1442,3 +1421,3 @@ @property

ucrtver = self.si.UniversalCRTSdkLastVersion
return (f'{ucrtver}\\') if ucrtver else ''
return ('%s\\' % ucrtver) if ucrtver else ''

@@ -1467,3 +1446,3 @@ @property

"""
vcruntime = f'vcruntime{self.vc_ver}0.dll'
vcruntime = 'vcruntime%d0.dll' % self.vc_ver
arch_subdir = self.pi.target_dir(x64=True).strip('\\')

@@ -1484,5 +1463,5 @@

crt_dirs = (
f'Microsoft.VC{self.vc_ver * 10}.CRT',
'Microsoft.VC%d.CRT' % (self.vc_ver * 10),
# Sometime store in directory with VS version instead of VC
f'Microsoft.VC{int(self.vs_ver) * 10}.CRT',
'Microsoft.VC%d.CRT' % (int(self.vs_ver) * 10),
)

@@ -1497,3 +1476,3 @@

def return_env(self, exists: bool = True) -> _EnvironmentDict:
def return_env(self, exists=True):
"""

@@ -1512,3 +1491,3 @@ Return environment dict.

"""
env = _EnvironmentDict(
env = dict(
include=self._build_paths(

@@ -1588,5 +1567,5 @@ 'include',

if not extant_paths:
msg = f"{name.upper()} environment variable is empty"
msg = "%s environment variable is empty" % name.upper()
raise distutils.errors.DistutilsPlatformError(msg)
unique_paths = unique_everseen(extant_paths)
return os.pathsep.join(unique_paths)

@@ -14,3 +14,3 @@ import itertools

def install_namespaces(self) -> None:
def install_namespaces(self):
nsp = self._get_all_ns_packages()

@@ -34,3 +34,3 @@ if not nsp:

def uninstall_namespaces(self) -> None:
def uninstall_namespaces(self):
filename = self._get_nspkg_file()

@@ -37,0 +37,0 @@ if not os.path.exists(filename):

@@ -1,3 +0,1 @@

from __future__ import annotations
import re

@@ -21,3 +19,3 @@ import time

def output_file(url: str, download_dir: Path = DOWNLOAD_DIR) -> Path:
def output_file(url: str, download_dir: Path = DOWNLOAD_DIR):
file_name = url.strip()

@@ -29,3 +27,3 @@ for part in NAME_REMOVE:

def retrieve_file(url: str, download_dir: Path = DOWNLOAD_DIR, wait: float = 5) -> Path:
def retrieve_file(url: str, download_dir: Path = DOWNLOAD_DIR, wait: float = 5):
path = output_file(url, download_dir)

@@ -45,3 +43,3 @@ if path.exists():

def urls_from_file(list_file: Path) -> list[str]:
def urls_from_file(list_file: Path):
"""``list_file`` should be a text file where each line corresponds to a URL to

@@ -48,0 +46,0 @@ download.

@@ -20,4 +20,3 @@ """Make sure that applying the configuration from pyproject.toml is equivalent to

import setuptools # noqa: F401 # ensure monkey patch to metadata
from setuptools._static import is_static
import setuptools # noqa ensure monkey patch to metadata
from setuptools.command.egg_info import write_requirements

@@ -27,4 +26,3 @@ from setuptools.config import expand, pyprojecttoml, setupcfg

from setuptools.dist import Distribution
from setuptools.errors import InvalidConfigError, RemovedConfigError
from setuptools.warnings import InformationOnly, SetuptoolsDeprecationWarning
from setuptools.errors import RemovedConfigError

@@ -41,10 +39,2 @@ from .downloads import retrieve_file, urls_from_file

def _mock_expand_patterns(patterns, *_, **__):
"""
Allow comparing the given patterns for 2 dist objects.
We need to strip special chars to avoid errors when validating.
"""
return [re.sub("[^a-z0-9]+", "", p, flags=re.I) or "empty" for p in patterns]
@pytest.mark.parametrize("url", urls_from_file(HERE / EXAMPLES_FILE))

@@ -55,5 +45,2 @@ @pytest.mark.filterwarnings("ignore")

monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.0.1"))
monkeypatch.setattr(
Distribution, "_expand_patterns", Mock(side_effect=_mock_expand_patterns)
)
setupcfg_example = retrieve_file(url)

@@ -111,3 +98,3 @@ pyproject_example = Path(tmp_path, "pyproject.toml")

requires-python = ">=3.8"
license-files = ["LICENSE.txt"] # Updated to be PEP 639 compliant
license = {file = "LICENSE.txt"}
keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]

@@ -176,29 +163,3 @@ authors = [

PEP639_LICENSE_TEXT = """\
[project]
name = "spam"
version = "2020.0.0"
authors = [
{email = "hi@pradyunsg.me"},
{name = "Tzu-Ping Chung"}
]
license = {text = "MIT"}
"""
PEP639_LICENSE_EXPRESSION = """\
[project]
name = "spam"
version = "2020.0.0"
authors = [
{email = "hi@pradyunsg.me"},
{name = "Tzu-Ping Chung"}
]
license = "mit or apache-2.0" # should be normalized in metadata
classifiers = [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python",
]
"""
def _pep621_example_project(

@@ -226,2 +187,3 @@ tmp_path,

dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
assert dist.metadata.license == "--- LICENSE stub ---"
assert set(dist.metadata.license_files) == {"LICENSE.txt"}

@@ -231,3 +193,3 @@

@pytest.mark.parametrize(
("readme", "ctype"),
"readme, ctype",
[

@@ -258,3 +220,3 @@ ("Readme.txt", "text/plain"),

@pytest.mark.parametrize(
("pyproject_text", "expected_maintainers_meta_value"),
('pyproject_text', 'expected_maintainers_meta_value'),
(

@@ -300,128 +262,18 @@ pytest.param(

@pytest.mark.parametrize(
(
'pyproject_text',
'license',
'license_expression',
'content_str',
'not_content_str',
),
(
pytest.param(
PEP639_LICENSE_TEXT,
'MIT',
None,
'License: MIT',
'License-Expression: ',
id='license-text',
marks=[
pytest.mark.filterwarnings(
"ignore:.project.license. as a TOML table is deprecated",
)
],
),
pytest.param(
PEP639_LICENSE_EXPRESSION,
None,
'MIT OR Apache-2.0',
'License-Expression: MIT OR Apache-2.0',
'License: ',
id='license-expression',
),
),
)
def test_license_in_metadata(
license,
license_expression,
content_str,
not_content_str,
pyproject_text,
tmp_path,
):
pyproject = _pep621_example_project(
tmp_path,
"README",
pyproject_text=pyproject_text,
)
dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
assert dist.metadata.license == license
assert dist.metadata.license_expression == license_expression
pkg_file = tmp_path / "PKG-FILE"
with open(pkg_file, "w", encoding="utf-8") as fh:
dist.metadata.write_pkg_file(fh)
content = pkg_file.read_text(encoding="utf-8")
assert "Metadata-Version: 2.4" in content
assert content_str in content
assert not_content_str not in content
class TestLicenseFiles:
# TODO: After PEP 639 is accepted, we have to move the license-files
# to the `project` table instead of `tool.setuptools`
def base_pyproject(self, tmp_path, additional_text):
pyproject = _pep621_example_project(tmp_path, "README")
text = pyproject.read_text(encoding="utf-8")
def test_license_classifier_with_license_expression(tmp_path):
text = PEP639_LICENSE_EXPRESSION.rsplit("\n", 2)[0]
pyproject = _pep621_example_project(
tmp_path,
"README",
f"{text}\n \"License :: OSI Approved :: MIT License\"\n]",
)
msg = "License classifiers have been superseded by license expressions"
with pytest.raises(InvalidConfigError, match=msg) as exc:
pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
assert "License :: OSI Approved :: MIT License" in str(exc.value)
def test_license_classifier_without_license_expression(tmp_path):
text = """\
[project]
name = "spam"
version = "2020.0.0"
license = {text = "mit or apache-2.0"}
classifiers = ["License :: OSI Approved :: MIT License"]
"""
pyproject = _pep621_example_project(tmp_path, "README", text)
msg1 = "License classifiers are deprecated(?:.|\n)*MIT License"
msg2 = ".project.license. as a TOML table is deprecated"
with (
pytest.warns(SetuptoolsDeprecationWarning, match=msg1),
pytest.warns(SetuptoolsDeprecationWarning, match=msg2),
):
dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
# Check license classifier is still included
assert dist.metadata.get_classifiers() == ["License :: OSI Approved :: MIT License"]
class TestLicenseFiles:
def base_pyproject(
self,
tmp_path,
additional_text="",
license_toml='license = {file = "LICENSE.txt"}\n',
):
text = PEP639_LICENSE_EXPRESSION
# Sanity-check
assert 'license = "mit or apache-2.0"' in text
assert 'license-files' not in text
assert 'license = {file = "LICENSE.txt"}' in text
assert "[tool.setuptools]" not in text
text = re.sub(
r"(license = .*)\n",
license_toml,
text,
count=1,
)
assert license_toml in text # sanity check
text = f"{text}\n{additional_text}\n"
pyproject = _pep621_example_project(tmp_path, "README", pyproject_text=text)
pyproject.write_text(text, encoding="utf-8")
return pyproject
def base_pyproject_license_pep639(self, tmp_path, additional_text=""):
return self.base_pyproject(
tmp_path,
additional_text=additional_text,
license_toml='license = "licenseref-Proprietary"'
'\nlicense-files = ["_FILE*"]\n',
)
def test_both_license_and_license_files_defined(self, tmp_path):

@@ -439,40 +291,10 @@ setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]'

msg1 = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'"
msg2 = ".project.license. as a TOML table is deprecated"
with (
pytest.warns(SetuptoolsDeprecationWarning, match=msg1),
pytest.warns(SetuptoolsDeprecationWarning, match=msg2),
):
dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"}
assert dist.metadata.license == "LicenseRef-Proprietary\n"
def test_both_license_and_license_files_defined_pep639(self, tmp_path):
# Set license and license-files
pyproject = self.base_pyproject_license_pep639(tmp_path)
(tmp_path / "_FILE.txt").touch()
(tmp_path / "_FILE.rst").touch()
msg = "Normalizing.*LicenseRef"
with pytest.warns(InformationOnly, match=msg):
dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"}
assert dist.metadata.license is None
assert dist.metadata.license_expression == "LicenseRef-Proprietary"
def test_license_files_defined_twice(self, tmp_path):
# Set project.license-files and tools.setuptools.license-files
setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]'
pyproject = self.base_pyproject_license_pep639(tmp_path, setuptools_config)
msg = "'project.license-files' is defined already. Remove 'tool.setuptools.license-files'"
with pytest.raises(InvalidConfigError, match=msg):
pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
def test_default_patterns(self, tmp_path):
setuptools_config = '[tool.setuptools]\nzip-safe = false'
# ^ used just to trigger section validation
pyproject = self.base_pyproject(tmp_path, setuptools_config, license_toml="")
pyproject = self.base_pyproject(tmp_path, setuptools_config)

@@ -485,36 +307,6 @@ license_files = "LICENCE-a.html COPYING-abc.txt AUTHORS-xyz NOTICE,def".split()

dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
assert (tmp_path / "LICENSE.txt").exists() # from base example
assert set(dist.metadata.license_files) == {*license_files, "LICENSE.txt"}
def test_missing_patterns(self, tmp_path):
pyproject = self.base_pyproject_license_pep639(tmp_path)
assert list(tmp_path.glob("_FILE*")) == [] # sanity check
msg1 = "Cannot find any files for the given pattern.*"
msg2 = "Normalizing 'licenseref-Proprietary' to 'LicenseRef-Proprietary'"
with (
pytest.warns(SetuptoolsDeprecationWarning, match=msg1),
pytest.warns(InformationOnly, match=msg2),
):
pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
def test_deprecated_file_expands_to_text(self, tmp_path):
"""Make sure the old example with ``license = {text = ...}`` works"""
assert 'license-files = ["LICENSE.txt"]' in PEP621_EXAMPLE # sanity check
text = PEP621_EXAMPLE.replace(
'license-files = ["LICENSE.txt"]',
'license = {file = "LICENSE.txt"}',
)
pyproject = _pep621_example_project(tmp_path, pyproject_text=text)
msg = ".project.license. as a TOML table is deprecated"
with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
assert dist.metadata.license == "--- LICENSE stub ---"
assert set(dist.metadata.license_files) == {"LICENSE.txt"} # auto-filled
class TestPyModules:

@@ -593,9 +385,4 @@ # https://github.com/pypa/setuptools/issues/4316

@pytest.mark.parametrize(
("attr", "field", "value"),
"attr, field, value",
[
("license_expression", "license", "MIT"),
pytest.param(
*("license", "license", "Not SPDX"),
marks=[pytest.mark.filterwarnings("ignore:.*license. overwritten")],
),
("classifiers", "classifiers", ["Private :: Classifier"]),

@@ -624,5 +411,4 @@ ("entry_points", "scripts", {"console_scripts": ["foobar=foobar:main"]}),

@pytest.mark.parametrize(
("attr", "field", "value"),
"attr, field, value",
[
("license_expression", "license", "MIT"),
("install_requires", "dependencies", []),

@@ -641,22 +427,2 @@ ("extras_require", "optional-dependencies", {}),

def test_license_files_exempt_from_dynamic(self, monkeypatch, tmp_path):
"""
license-file is currently not considered in the context of dynamic.
As per 2025-02-19, https://packaging.python.org/en/latest/specifications/pyproject-toml/#license-files
allows setuptools to fill-in `license-files` the way it sees fit:
> If the license-files key is not defined, tools can decide how to handle license files.
> For example they can choose not to include any files or use their own
> logic to discover the appropriate files in the distribution.
Using license_files from setup.py to fill-in the value is in accordance
with this rule.
"""
monkeypatch.chdir(tmp_path)
pyproject = self.pyproject(tmp_path, [])
dist = makedist(tmp_path, license_files=["LIC*"])
(tmp_path / "LIC1").write_text("42", encoding="utf-8")
dist = pyprojecttoml.apply_configuration(dist, pyproject)
assert dist.metadata.license_files == ["LIC1"]
def test_warning_overwritten_dependencies(self, tmp_path):

@@ -694,4 +460,3 @@ src = "[project]\nname='pkg'\nversion='0.1'\ndependencies=['click']\n"

@pytest.mark.parametrize(
("field", "group"),
[("scripts", "console_scripts"), ("gui-scripts", "gui_scripts")],
"field,group", [("scripts", "console_scripts"), ("gui-scripts", "gui_scripts")]
)

@@ -733,28 +498,2 @@ @pytest.mark.filterwarnings("error")

class TestStaticConfig:
def test_mark_static_fields(self, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
toml_config = """
[project]
name = "test"
version = "42.0"
dependencies = ["hello"]
keywords = ["world"]
classifiers = ["private :: hello world"]
[tool.setuptools]
obsoletes = ["abcd"]
provides = ["abcd"]
platforms = ["abcd"]
"""
pyproject = Path(tmp_path, "pyproject.toml")
pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
dist = pyprojecttoml.apply_configuration(Distribution({}), pyproject)
assert is_static(dist.install_requires)
assert is_static(dist.metadata.keywords)
assert is_static(dist.metadata.classifiers)
assert is_static(dist.metadata.obsoletes)
assert is_static(dist.metadata.provides)
assert is_static(dist.metadata.platforms)
# --- Auxiliary Functions ---

@@ -761,0 +500,0 @@

@@ -7,3 +7,2 @@ import os

from setuptools._static import is_static
from setuptools.config import expand

@@ -98,11 +97,7 @@ from setuptools.discovery import find_package_path

# Make sure it can read the attr statically without evaluating the module
version = expand.read_attr('pkg.sub.VERSION')
assert expand.read_attr('pkg.sub.VERSION') == '0.1.1'
values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'})
assert version == '0.1.1'
assert is_static(values)
assert values['a'] == 0
assert values['b'] == {42}
assert is_static(values)

@@ -128,25 +123,4 @@ # Make sure the same APIs work outside cwd

# Make sure this attribute can be read statically
version = expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path)
assert version == '0.1.1'
assert is_static(version)
assert expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path) == '0.1.1'
@pytest.mark.parametrize(
"example",
[
"VERSION = (lambda: '0.1.1')()\n",
"def fn(): return '0.1.1'\nVERSION = fn()\n",
"VERSION: str = (lambda: '0.1.1')()\n",
],
)
def test_read_dynamic_attr(self, tmp_path, monkeypatch, example):
files = {
"pkg/__init__.py": "",
"pkg/sub/__init__.py": example,
}
write_files(files, tmp_path)
monkeypatch.chdir(tmp_path)
version = expand.read_attr('pkg.sub.VERSION')
assert version == '0.1.1'
assert not is_static(version)
def test_import_order(self, tmp_path):

@@ -173,3 +147,3 @@ """

@pytest.mark.parametrize(
("package_dir", "file", "module", "return_value"),
'package_dir, file, module, return_value',
[

@@ -191,3 +165,3 @@ ({"": "src"}, "src/pkg/main.py", "pkg.main", 42),

@pytest.mark.parametrize(
("args", "pkgs"),
'args, pkgs',
[

@@ -226,3 +200,3 @@ ({"where": ["."], "namespaces": False}, {"pkg", "other"}),

@pytest.mark.parametrize(
("files", "where", "expected_package_dir"),
"files, where, expected_package_dir",
[

@@ -229,0 +203,0 @@ (["pkg1/__init__.py", "pkg1/other.py"], ["."], {}),

@@ -152,3 +152,3 @@ import re

@pytest.mark.parametrize(
("pkg_root", "opts"),
"pkg_root, opts",
[

@@ -312,3 +312,3 @@ (".", {}),

@pytest.mark.parametrize(
("example", "error_msg"),
"example, error_msg",
[

@@ -315,0 +315,0 @@ (

import configparser
import contextlib
import inspect
import re
from pathlib import Path

@@ -91,3 +90,3 @@ from unittest.mock import Mock, patch

)
config_dict = read_configuration(str(config))
config_dict = read_configuration('%s' % config)
assert config_dict['metadata']['version'] == '10.1.1'

@@ -99,3 +98,3 @@ assert config_dict['metadata']['keywords'] == ['one', 'two']

with pytest.raises(DistutilsFileError):
read_configuration(str(tmpdir.join('setup.cfg')))
read_configuration('%s' % tmpdir.join('setup.cfg'))

@@ -108,5 +107,5 @@ def test_ignore_errors(self, tmpdir):

with pytest.raises(ImportError):
read_configuration(str(config))
read_configuration('%s' % config)
config_dict = read_configuration(str(config), ignore_option_errors=True)
config_dict = read_configuration('%s' % config, ignore_option_errors=True)

@@ -295,3 +294,5 @@ assert config_dict['metadata']['keywords'] == ['one', 'two']

def test_version_file(self, tmpdir):
fake_env(tmpdir, '[metadata]\nversion = file: fake_package/version.txt\n')
_, config = fake_env(
tmpdir, '[metadata]\nversion = file: fake_package/version.txt\n'
)
tmpdir.join('fake_package', 'version.txt').write('1.2.3\n')

@@ -308,3 +309,3 @@

def test_version_with_package_dir_simple(self, tmpdir):
fake_env(
_, config = fake_env(
tmpdir,

@@ -323,3 +324,3 @@ '[metadata]\n'

def test_version_with_package_dir_rename(self, tmpdir):
fake_env(
_, config = fake_env(
tmpdir,

@@ -338,3 +339,3 @@ '[metadata]\n'

def test_version_with_package_dir_complex(self, tmpdir):
fake_env(
_, config = fake_env(
tmpdir,

@@ -431,46 +432,34 @@ '[metadata]\n'

@pytest.mark.parametrize(
("error_msg", "config", "invalid"),
[
(
"Invalid dash-separated key 'author-email' in 'metadata' (setup.cfg)",
DALS(
"""
[metadata]
author-email = test@test.com
maintainer_email = foo@foo.com
"""
),
{"author-email": "test@test.com"},
),
(
"Invalid uppercase key 'Name' in 'metadata' (setup.cfg)",
DALS(
"""
[metadata]
Name = foo
description = Some description
"""
),
{"Name": "foo"},
),
],
)
def test_invalid_options_previously_deprecated(
self, tmpdir, error_msg, config, invalid
):
# This test and related methods can be removed when no longer needed.
# Deprecation postponed due to push-back from the community in
# https://github.com/pypa/setuptools/issues/4910
fake_env(tmpdir, config)
with pytest.warns(SetuptoolsDeprecationWarning, match=re.escape(error_msg)):
dist = get_dist(tmpdir).__enter__()
@pytest.mark.xfail(reason="#4864")
def test_warn_dash_deprecation(self, tmpdir):
# warn_dash_deprecation() is a method in setuptools.dist
# remove this test and the method when no longer needed
fake_env(
tmpdir,
'[metadata]\n'
'author-email = test@test.com\n'
'maintainer_email = foo@foo.com\n',
)
msg = "Usage of dash-separated 'author-email' will not be supported"
with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
with get_dist(tmpdir) as dist:
metadata = dist.metadata
tmpdir.join('setup.cfg').remove()
assert metadata.author_email == 'test@test.com'
assert metadata.maintainer_email == 'foo@foo.com'
for field, value in invalid.items():
attr = field.replace("-", "_").lower()
assert getattr(dist.metadata, attr) == value
@pytest.mark.xfail(reason="#4864")
def test_make_option_lowercase(self, tmpdir):
# remove this test and the method make_option_lowercase() in setuptools.dist
# when no longer needed
fake_env(tmpdir, '[metadata]\nName = foo\ndescription = Some description\n')
msg = "Usage of uppercase key 'Name' in 'metadata' will not be supported"
with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
with get_dist(tmpdir) as dist:
metadata = dist.metadata
assert metadata.name == 'foo'
assert metadata.description == 'Some description'
class TestOptions:

@@ -609,4 +598,4 @@ def test_basic(self, tmpdir):

make_package_dir('sub_one', dir_package)
make_package_dir('sub_two', dir_package)
dir_sub_one, _ = make_package_dir('sub_one', dir_package)
dir_sub_two, _ = make_package_dir('sub_two', dir_package)

@@ -649,4 +638,4 @@ with get_dist(tmpdir) as dist:

make_package_dir('sub_one', dir_package)
make_package_dir('sub_two', dir_package, ns=True)
dir_sub_one, _ = make_package_dir('sub_one', dir_package)
dir_sub_two, _ = make_package_dir('sub_two', dir_package, ns=True)

@@ -805,3 +794,3 @@ with get_dist(tmpdir) as dist:

def test_case_sensitive_entry_points(self, tmpdir):
fake_env(
_, config = fake_env(
tmpdir,

@@ -808,0 +797,0 @@ '[options.entry_points]\n'

@@ -78,2 +78,16 @@ import contextlib

@contextlib.contextmanager
def save_pkg_resources_state():
import pkg_resources
pr_state = pkg_resources.__getstate__()
# also save sys.path
sys_path = sys.path[:]
try:
yield pr_state, sys_path
finally:
sys.path[:] = sys_path
pkg_resources.__setstate__(pr_state)
@contextlib.contextmanager
def suppress_exceptions(*excs):

@@ -80,0 +94,0 @@ try:

import contextlib
import io
import os
import subprocess
import sys
import tarfile
import time
from pathlib import Path
import jaraco.path
import path
import pytest
from setuptools._normalization import safer_name
from . import contexts, environment
from .textwrap import DALS

@@ -70,8 +63,2 @@

@pytest.fixture
def sample_project_cwd(sample_project):
with path.Path(sample_project):
yield
# sdist and wheel artifacts should be stable across a round of tests

@@ -172,223 +159,1 @@ # so we can build them once per session and use the files as "readonly"

return env
def make_sdist(dist_path, files):
"""
Create a simple sdist tarball at dist_path, containing the files
listed in ``files`` as ``(filename, content)`` tuples.
"""
# Distributions with only one file don't play well with pip.
assert len(files) > 1
with tarfile.open(dist_path, 'w:gz') as dist:
for filename, content in files:
file_bytes = io.BytesIO(content.encode('utf-8'))
file_info = tarfile.TarInfo(name=filename)
file_info.size = len(file_bytes.getvalue())
file_info.mtime = int(time.time())
dist.addfile(file_info, fileobj=file_bytes)
def make_trivial_sdist(dist_path, distname, version):
"""
Create a simple sdist tarball at dist_path, containing just a simple
setup.py.
"""
make_sdist(
dist_path,
[
(
'setup.py',
DALS(
f"""\
import setuptools
setuptools.setup(
name={distname!r},
version={version!r}
)
"""
),
),
('setup.cfg', ''),
],
)
def make_nspkg_sdist(dist_path, distname, version):
"""
Make an sdist tarball with distname and version which also contains one
package with the same name as distname. The top-level package is
designated a namespace package).
"""
# Assert that the distname contains at least one period
assert '.' in distname
parts = distname.split('.')
nspackage = parts[0]
packages = ['.'.join(parts[:idx]) for idx in range(1, len(parts) + 1)]
setup_py = DALS(
f"""\
import setuptools
setuptools.setup(
name={distname!r},
version={version!r},
packages={packages!r},
namespace_packages=[{nspackage!r}]
)
"""
)
init = "__import__('pkg_resources').declare_namespace(__name__)"
files = [('setup.py', setup_py), (os.path.join(nspackage, '__init__.py'), init)]
for package in packages[1:]:
filename = os.path.join(*(package.split('.') + ['__init__.py']))
files.append((filename, ''))
make_sdist(dist_path, files)
def make_python_requires_sdist(dist_path, distname, version, python_requires):
make_sdist(
dist_path,
[
(
'setup.py',
DALS(
"""\
import setuptools
setuptools.setup(
name={name!r},
version={version!r},
python_requires={python_requires!r},
)
"""
).format(
name=distname, version=version, python_requires=python_requires
),
),
('setup.cfg', ''),
],
)
def create_setup_requires_package(
path,
distname='foobar',
version='0.1',
make_package=make_trivial_sdist,
setup_py_template=None,
setup_attrs=None,
use_setup_cfg=(),
):
"""Creates a source tree under path for a trivial test package that has a
single requirement in setup_requires--a tarball for that requirement is
also created and added to the dependency_links argument.
``distname`` and ``version`` refer to the name/version of the package that
the test package requires via ``setup_requires``. The name of the test
package itself is just 'test_pkg'.
"""
normalized_distname = safer_name(distname)
test_setup_attrs = {
'name': 'test_pkg',
'version': '0.0',
'setup_requires': [f'{normalized_distname}=={version}'],
'dependency_links': [os.path.abspath(path)],
}
if setup_attrs:
test_setup_attrs.update(setup_attrs)
test_pkg = os.path.join(path, 'test_pkg')
os.mkdir(test_pkg)
# setup.cfg
if use_setup_cfg:
options = []
metadata = []
for name in use_setup_cfg:
value = test_setup_attrs.pop(name)
if name in 'name version'.split():
section = metadata
else:
section = options
if isinstance(value, (tuple, list)):
value = ';'.join(value)
section.append(f'{name}: {value}')
test_setup_cfg_contents = DALS(
"""
[metadata]
{metadata}
[options]
{options}
"""
).format(
options='\n'.join(options),
metadata='\n'.join(metadata),
)
else:
test_setup_cfg_contents = ''
with open(os.path.join(test_pkg, 'setup.cfg'), 'w', encoding="utf-8") as f:
f.write(test_setup_cfg_contents)
# setup.py
if setup_py_template is None:
setup_py_template = DALS(
"""\
import setuptools
setuptools.setup(**%r)
"""
)
with open(os.path.join(test_pkg, 'setup.py'), 'w', encoding="utf-8") as f:
f.write(setup_py_template % test_setup_attrs)
foobar_path = os.path.join(path, f'{normalized_distname}-{version}.tar.gz')
make_package(foobar_path, distname, version)
return test_pkg
@pytest.fixture
def pbr_package(tmp_path, monkeypatch, venv):
files = {
"pyproject.toml": DALS(
"""
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
"""
),
"setup.py": DALS(
"""
__import__('setuptools').setup(
pbr=True,
setup_requires=["pbr"],
)
"""
),
"setup.cfg": DALS(
"""
[metadata]
name = mypkg
[files]
packages =
mypkg
"""
),
"mypkg": {
"__init__.py": "",
"hello.py": "print('Hello world!')",
},
"other": {"test.txt": "Another file in here."},
}
venv.run(["python", "-m", "pip", "install", "pbr"])
prefix = tmp_path / 'mypkg'
prefix.mkdir()
jaraco.path.build(files, prefix=prefix)
monkeypatch.setenv('PBR_VERSION', "0.42")
return prefix

@@ -57,3 +57,3 @@ # https://github.com/python/mypy/issues/16936

("protobuf", LATEST),
# ("requests", LATEST), # XXX: https://github.com/psf/requests/pull/6920
("requests", LATEST),
("celery", LATEST),

@@ -108,3 +108,3 @@ # When adding packages to this list, make sure they expose a `__version__`

@pytest.fixture(autouse=True)
def _prepare(tmp_path, venv_python, monkeypatch):
def _prepare(tmp_path, venv_python, monkeypatch, request):
download_path = os.getenv("DOWNLOAD_PATH", str(tmp_path))

@@ -116,14 +116,15 @@ os.makedirs(download_path, exist_ok=True)

yield
def _debug_info():
# Let's provide the maximum amount of information possible in the case
# it is necessary to debug the tests directly from the CI logs.
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
print("Temporary directory:")
map(print, tmp_path.glob("*"))
print("Virtual environment:")
run([venv_python, "-m", "pip", "freeze"])
# Let's provide the maximum amount of information possible in the case
# it is necessary to debug the tests directly from the CI logs.
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
print("Temporary directory:")
map(print, tmp_path.glob("*"))
print("Virtual environment:")
run([venv_python, "-m", "pip", "freeze"])
request.addfinalizer(_debug_info)
@pytest.mark.parametrize(("package", "version"), EXAMPLES)
@pytest.mark.parametrize('package, version', EXAMPLES)
@pytest.mark.uses_network

@@ -130,0 +131,0 @@ def test_install_sdist(package, version, tmp_path, venv_python, setuptools_wheel):

@@ -20,3 +20,3 @@ """develop tests"""

@pytest.fixture
@pytest.fixture(scope='function')
def setup_context(tmpdir):

@@ -32,5 +32,3 @@ with (tmpdir / 'setup.py').open('w') as f:

class Test:
@pytest.mark.usefixtures("user_override")
@pytest.mark.usefixtures("setup_context")
def test_bdist_egg(self):
def test_bdist_egg(self, setup_context, user_override):
dist = Distribution(

@@ -57,5 +55,3 @@ dict(

)
@pytest.mark.usefixtures("user_override")
@pytest.mark.usefixtures("setup_context")
def test_exclude_source_files(self):
def test_exclude_source_files(self, setup_context, user_override):
dist = Distribution(

@@ -62,0 +58,0 @@ dict(

@@ -14,2 +14,3 @@ from __future__ import annotations

from inspect import cleandoc
from unittest.mock import Mock
from zipfile import ZipFile

@@ -22,3 +23,8 @@

import setuptools
from setuptools.command.bdist_wheel import bdist_wheel, get_abi_tag
from setuptools.command.bdist_wheel import (
bdist_wheel,
get_abi_tag,
remove_readonly,
remove_readonly_exc,
)
from setuptools.dist import Distribution

@@ -64,3 +70,3 @@ from setuptools.warnings import SetuptoolsDeprecationWarning

"setup.py": SETUPPY_EXAMPLE,
"licenses_dir": {"DUMMYFILE": ""},
"licenses": {"DUMMYFILE": ""},
**dict.fromkeys(DEFAULT_LICENSE_FILES | OTHER_IGNORED_FILES, ""),

@@ -178,16 +184,2 @@ },

},
"licenses-dist": {
"setup.cfg": cleandoc(
"""
[metadata]
name = licenses-dist
version = 1.0
license_files = **/LICENSE
"""
),
"LICENSE": "",
"src": {
"vendor": {"LICENSE": ""},
},
},
}

@@ -259,7 +251,2 @@

@pytest.fixture
def licenses_dist(tmp_path_factory):
return mkexample(tmp_path_factory, "licenses-dist")
def test_no_scripts(wheel_paths):

@@ -324,4 +311,3 @@ """Make sure entry point scripts are not generated."""

license_files = {
"dummy_dist-1.0.dist-info/licenses/" + fname
for fname in DEFAULT_LICENSE_FILES
"dummy_dist-1.0.dist-info/" + fname for fname in DEFAULT_LICENSE_FILES
}

@@ -333,3 +319,3 @@ assert set(wf.namelist()) == DEFAULT_FILES | license_files

dummy_dist.joinpath("setup.cfg").write_text(
"[metadata]\nlicense_file=licenses_dir/DUMMYFILE", encoding="utf-8"
"[metadata]\nlicense_file=licenses/DUMMYFILE", encoding="utf-8"
)

@@ -341,3 +327,3 @@ monkeypatch.chdir(dummy_dist)

with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:
license_files = {"dummy_dist-1.0.dist-info/licenses/licenses_dir/DUMMYFILE"}
license_files = {"dummy_dist-1.0.dist-info/DUMMYFILE"}
assert set(wf.namelist()) == DEFAULT_FILES | license_files

@@ -347,10 +333,10 @@

@pytest.mark.parametrize(
("config_file", "config"),
"config_file, config",
[
("setup.cfg", "[metadata]\nlicense_files=licenses_dir/*\n LICENSE"),
("setup.cfg", "[metadata]\nlicense_files=licenses_dir/*, LICENSE"),
("setup.cfg", "[metadata]\nlicense_files=licenses/*\n LICENSE"),
("setup.cfg", "[metadata]\nlicense_files=licenses/*, LICENSE"),
(
"setup.py",
SETUPPY_EXAMPLE.replace(
")", " license_files=['licenses_dir/DUMMYFILE', 'LICENSE'])"
")", " license_files=['licenses/DUMMYFILE', 'LICENSE'])"
),

@@ -366,27 +352,7 @@ ),

license_files = {
"dummy_dist-1.0.dist-info/licenses/" + fname
for fname in {"licenses_dir/DUMMYFILE", "LICENSE"}
"dummy_dist-1.0.dist-info/" + fname for fname in {"DUMMYFILE", "LICENSE"}
}
assert set(wf.namelist()) == DEFAULT_FILES | license_files
metadata = wf.read("dummy_dist-1.0.dist-info/METADATA").decode("utf8")
assert "License-File: licenses_dir/DUMMYFILE" in metadata
assert "License-File: LICENSE" in metadata
def test_licenses_preserve_folder_structure(licenses_dist, monkeypatch, tmp_path):
monkeypatch.chdir(licenses_dist)
bdist_wheel_cmd(bdist_dir=str(tmp_path)).run()
print(os.listdir("dist"))
with ZipFile("dist/licenses_dist-1.0-py3-none-any.whl") as wf:
default_files = {name.replace("dummy_", "licenses_") for name in DEFAULT_FILES}
license_files = {
"licenses_dist-1.0.dist-info/licenses/LICENSE",
"licenses_dist-1.0.dist-info/licenses/src/vendor/LICENSE",
}
assert set(wf.namelist()) == default_files | license_files
metadata = wf.read("licenses_dist-1.0.dist-info/METADATA").decode("utf8")
assert "License-File: src/vendor/LICENSE" in metadata
assert "License-File: LICENSE" in metadata
def test_licenses_disabled(dummy_dist, monkeypatch, tmp_path):

@@ -481,3 +447,3 @@ dummy_dist.joinpath("setup.cfg").write_text(

@pytest.mark.parametrize(
("option", "compress_type"),
"option, compress_type",
list(bdist_wheel.supported_compressions.items()),

@@ -558,2 +524,25 @@ ids=list(bdist_wheel.supported_compressions),

def test_rmtree_readonly(monkeypatch, tmp_path):
"""Verify onerr works as expected"""
bdist_dir = tmp_path / "with_readonly"
bdist_dir.mkdir()
some_file = bdist_dir.joinpath("file.txt")
some_file.touch()
some_file.chmod(stat.S_IREAD)
expected_count = 1 if sys.platform.startswith("win") else 0
if sys.version_info < (3, 12):
count_remove_readonly = Mock(side_effect=remove_readonly)
shutil.rmtree(bdist_dir, onerror=count_remove_readonly)
assert count_remove_readonly.call_count == expected_count
else:
count_remove_readonly_exc = Mock(side_effect=remove_readonly_exc)
shutil.rmtree(bdist_dir, onexc=count_remove_readonly_exc)
assert count_remove_readonly_exc.call_count == expected_count
assert not bdist_dir.is_dir()
def test_data_dir_with_tag_build(monkeypatch, tmp_path):

@@ -615,3 +604,3 @@ """

@pytest.mark.parametrize(
("reported", "expected"),
"reported,expected",
[("linux-x86_64", "linux_i686"), ("linux-aarch64", "linux_armv7l")],

@@ -677,46 +666,1 @@ )

assert not [path for path in files_found if 'egg-info' in str(path)]
def test_allow_grace_period_parent_directory_license(monkeypatch, tmp_path):
# Motivation: https://github.com/pypa/setuptools/issues/4892
# TODO: Remove this test after deprecation period is over
files = {
"LICENSE.txt": "parent license", # <---- the license files are outside
"NOTICE.txt": "parent notice",
"python": {
"pyproject.toml": cleandoc(
"""
[project]
name = "test-proj"
dynamic = ["version"] # <---- testing dynamic will not break
[tool.setuptools.dynamic]
version.file = "VERSION"
"""
),
"setup.cfg": cleandoc(
"""
[metadata]
license_files =
../LICENSE.txt
../NOTICE.txt
"""
),
"VERSION": "42",
},
}
jaraco.path.build(files, prefix=str(tmp_path))
monkeypatch.chdir(tmp_path / "python")
msg = "Pattern '../.*.txt' cannot contain '..'"
with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
bdist_wheel_cmd().run()
with ZipFile("dist/test_proj-42-py3-none-any.whl") as wf:
files_found = set(wf.namelist())
expected_files = {
"test_proj-42.dist-info/licenses/LICENSE.txt",
"test_proj-42.dist-info/licenses/NOTICE.txt",
}
assert expected_files <= files_found
metadata = wf.read("test_proj-42.dist-info/METADATA").decode("utf8")
assert "License-File: LICENSE.txt" in metadata
assert "License-File: NOTICE.txt" in metadata

@@ -1,3 +0,1 @@

from __future__ import annotations
import os

@@ -183,8 +181,8 @@ import sys

class TestBuildExtInplace:
def get_build_ext_cmd(self, optional: bool, **opts) -> build_ext:
files: dict[str, str | dict[str, dict[str, str]]] = {
def get_build_ext_cmd(self, optional: bool, **opts):
files = {
"eggs.c": "#include missingheader.h\n",
".build": {"lib": {}, "tmp": {}},
}
path.build(files)
path.build(files) # jaraco/path#232
extension = Extension('spam.eggs', ['eggs.c'], optional=optional)

@@ -290,6 +288,6 @@ dist = Distribution(dict(ext_modules=[extension]))

path.build(files)
code, (stdout, stderr) = environment.run_setup_py(
code, output = environment.run_setup_py(
cmd=['build'],
data_stream=(0, 2),
)
assert code == 0, f'\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}'
assert code == 0, '\nSTDOUT:\n%s\nSTDERR:\n%s' % output

@@ -9,3 +9,2 @@ import contextlib

import tarfile
import warnings
from concurrent import futures

@@ -20,4 +19,2 @@ from pathlib import Path

from setuptools.warnings import SetuptoolsDeprecationWarning
from .textwrap import DALS

@@ -392,11 +389,4 @@

path.build(files)
msgs = [
"'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'",
"`project.license` as a TOML table is deprecated",
]
with warnings.catch_warnings():
for msg in msgs:
warnings.filterwarnings("ignore", msg, SetuptoolsDeprecationWarning)
sdist_path = build_backend.build_sdist("temp")
wheel_file = build_backend.build_wheel("temp")
sdist_path = build_backend.build_sdist("temp")
wheel_file = build_backend.build_wheel("temp")

@@ -409,5 +399,3 @@ with tarfile.open(os.path.join(tmpdir, "temp", sdist_path)) as tar:

metadata = str(zipfile.read("foo-0.1.dist-info/METADATA"), "utf-8")
license = str(
zipfile.read("foo-0.1.dist-info/licenses/LICENSE.txt"), "utf-8"
)
license = str(zipfile.read("foo-0.1.dist-info/LICENSE.txt"), "utf-8")
epoints = str(zipfile.read("foo-0.1.dist-info/entry_points.txt"), "utf-8")

@@ -445,3 +433,3 @@

"foo/py.typed", # include type information by default
"foo-0.1.dist-info/licenses/LICENSE.txt",
"foo-0.1.dist-info/LICENSE.txt",
"foo-0.1.dist-info/METADATA",

@@ -458,3 +446,2 @@ "foo-0.1.dist-info/WHEEL",

"License: MIT",
"License-File: LICENSE.txt",
"Classifier: Intended Audience :: Developers",

@@ -759,3 +746,3 @@ "Requires-Dist: appdirs",

@pytest.mark.parametrize(
("setup_literal", "requirements"),
'setup_literal, requirements',
[

@@ -946,2 +933,26 @@ ("'foo'", ['foo']),

def test_legacy_editable_install(venv, tmpdir, tmpdir_cwd):
pyproject = """
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "myproj"
version = "42"
"""
path.build({"pyproject.toml": DALS(pyproject), "mymod.py": ""})
# First: sanity check
cmd = ["pip", "install", "--no-build-isolation", "-e", "."]
output = venv.run(cmd, cwd=tmpdir).lower()
assert "running setup.py develop for myproj" not in output
assert "created wheel for myproj" in output
# Then: real test
env = {**os.environ, "SETUPTOOLS_ENABLE_FEATURES": "legacy-editable"}
cmd = ["pip", "install", "--no-build-isolation", "-e", "."]
output = venv.run(cmd, cwd=tmpdir, env=env).lower()
assert "running setup.py develop for myproj" in output
@pytest.mark.filterwarnings("ignore::setuptools.SetuptoolsDeprecationWarning")

@@ -948,0 +959,0 @@ def test_sys_exit_0_in_setuppy(monkeypatch, tmp_path):

@@ -171,3 +171,3 @@ import os

# existing behavior of `include_package_data`. After the transition, we
# should remove the warning and fix the behavior.
# should remove the warning and fix the behaviour.

@@ -174,0 +174,0 @@ if os.getenv("SETUPTOOLS_USE_DISTUTILS") == "stdlib":

@@ -164,3 +164,3 @@ import os

@pytest.mark.parametrize(
("config_file", "param", "circumstance"),
"config_file, param, circumstance",
product(

@@ -195,3 +195,3 @@ ["setup.cfg", "setup.py", "pyproject.toml"],

@pytest.mark.parametrize(
("extra_files", "pkgs"),
"extra_files, pkgs",
[

@@ -289,3 +289,3 @@ (["venv/bin/simulate_venv"], {"pkg"}),

@pytest.mark.parametrize(
("folder", "opts"),
"folder, opts",
[

@@ -452,3 +452,3 @@ ("src", {}),

@pytest.mark.parametrize(
("src_root", "files"),
"src_root, files",
[

@@ -455,0 +455,0 @@ (".", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}),

@@ -1,3 +0,1 @@

from __future__ import annotations
import functools

@@ -7,14 +5,5 @@ import importlib

from email import message_from_string
from email.generator import Generator
from email.message import EmailMessage, Message
from email.parser import Parser
from email.policy import EmailPolicy
from inspect import cleandoc
from pathlib import Path
from unittest.mock import Mock
import jaraco.path
import pytest
from packaging.metadata import Metadata
from packaging.requirements import Requirement

@@ -24,7 +13,4 @@ from setuptools import _reqs, sic

from setuptools.command.egg_info import egg_info, write_requirements
from setuptools.config import expand, setupcfg
from setuptools.dist import Distribution
from .config.downloads import retrieve_file, urls_from_file
EXAMPLE_BASE_INFO = dict(

@@ -42,3 +28,3 @@ name="package",

@pytest.mark.parametrize(
("content", "result"),
'content, result',
(

@@ -174,3 +160,3 @@ pytest.param(

@pytest.mark.parametrize(("name", "attrs"), __read_test_cases())
@pytest.mark.parametrize('name,attrs', __read_test_cases())
def test_read_metadata(name, attrs):

@@ -284,3 +270,3 @@ dist = Distribution(attrs)

@pytest.mark.parametrize(("name", "attrs"), __maintainer_test_cases())
@pytest.mark.parametrize('name,attrs', __maintainer_test_cases())
def test_maintainer_author(name, attrs, tmpdir):

@@ -321,308 +307,88 @@ tested_keys = {

else:
line = f'{fkey}: {val}'
line = '%s: %s' % (fkey, val)
assert line in pkg_lines_set
class TestParityWithMetadataFromPyPaWheel:
def base_example(self):
attrs = dict(
**EXAMPLE_BASE_INFO,
# Example with complex requirement definition
python_requires=">=3.8",
install_requires="""
packaging==23.2
more-itertools==8.8.0; extra == "other"
jaraco.text==3.7.0
importlib-resources==5.10.2; python_version<"3.8"
importlib-metadata==6.0.0 ; python_version<"3.8"
colorama>=0.4.4; sys_platform == "win32"
""",
extras_require={
"testing": """
pytest >= 6
pytest-checkdocs >= 2.4
tomli ; \\
# Using stdlib when possible
python_version < "3.11"
ini2toml[lite]>=0.9
""",
"other": [],
},
)
# Generate a PKG-INFO file using setuptools
return Distribution(attrs)
def test_requires_dist(self, tmp_path):
dist = self.base_example()
pkg_info = _get_pkginfo(dist)
assert _valid_metadata(pkg_info)
# Ensure Requires-Dist is present
expected = [
'Metadata-Version:',
'Requires-Python: >=3.8',
'Provides-Extra: other',
'Provides-Extra: testing',
'Requires-Dist: tomli; python_version < "3.11" and extra == "testing"',
'Requires-Dist: more-itertools==8.8.0; extra == "other"',
'Requires-Dist: ini2toml[lite]>=0.9; extra == "testing"',
]
for line in expected:
assert line in pkg_info
HERE = Path(__file__).parent
EXAMPLES_FILE = HERE / "config/setupcfg_examples.txt"
@pytest.fixture(params=[None, *urls_from_file(EXAMPLES_FILE)])
def dist(self, request, monkeypatch, tmp_path):
"""Example of distribution with arbitrary configuration"""
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.42"))
monkeypatch.setattr(expand, "read_files", Mock(return_value="hello world"))
monkeypatch.setattr(
Distribution, "_finalize_license_files", Mock(return_value=None)
)
if request.param is None:
yield self.base_example()
else:
# Real-world usage
config = retrieve_file(request.param)
yield setupcfg.apply_configuration(Distribution({}), config)
@pytest.mark.uses_network
def test_equivalent_output(self, tmp_path, dist):
"""Ensure output from setuptools is equivalent to the one from `pypa/wheel`"""
# Generate a METADATA file using pypa/wheel for comparison
wheel_metadata = importlib.import_module("wheel.metadata")
pkginfo_to_metadata = getattr(wheel_metadata, "pkginfo_to_metadata", None)
if pkginfo_to_metadata is None: # pragma: nocover
pytest.xfail(
"wheel.metadata.pkginfo_to_metadata is undefined, "
"(this is likely to be caused by API changes in pypa/wheel"
)
# Generate an simplified "egg-info" dir for pypa/wheel to convert
pkg_info = _get_pkginfo(dist)
egg_info_dir = tmp_path / "pkg.egg-info"
egg_info_dir.mkdir(parents=True)
(egg_info_dir / "PKG-INFO").write_text(pkg_info, encoding="utf-8")
write_requirements(egg_info(dist), egg_info_dir, egg_info_dir / "requires.txt")
# Get pypa/wheel generated METADATA but normalize requirements formatting
metadata_msg = pkginfo_to_metadata(egg_info_dir, egg_info_dir / "PKG-INFO")
metadata_str = _normalize_metadata(metadata_msg)
pkg_info_msg = message_from_string(pkg_info)
pkg_info_str = _normalize_metadata(pkg_info_msg)
# Compare setuptools PKG-INFO x pypa/wheel METADATA
assert metadata_str == pkg_info_str
# Make sure it parses/serializes well in pypa/wheel
_assert_roundtrip_message(pkg_info)
class TestPEP643:
STATIC_CONFIG = {
"setup.cfg": cleandoc(
"""
[metadata]
name = package
version = 0.0.1
author = Foo Bar
author_email = foo@bar.net
long_description = Long
description
description = Short description
keywords = one, two
platforms = abcd
[options]
install_requires = requests
"""
),
"pyproject.toml": cleandoc(
"""
[project]
name = "package"
version = "0.0.1"
authors = [
{name = "Foo Bar", email = "foo@bar.net"}
]
description = "Short description"
readme = {text = "Long\\ndescription", content-type = "text/plain"}
keywords = ["one", "two"]
dependencies = ["requests"]
license = "AGPL-3.0-or-later"
[tool.setuptools]
provides = ["abcd"]
obsoletes = ["abcd"]
"""
),
}
@pytest.mark.parametrize("file", STATIC_CONFIG.keys())
def test_static_config_has_no_dynamic(self, file, tmpdir_cwd):
Path(file).write_text(self.STATIC_CONFIG[file], encoding="utf-8")
metadata = _get_metadata()
assert metadata.get_all("Dynamic") is None
assert metadata.get_all("dynamic") is None
@pytest.mark.parametrize("file", STATIC_CONFIG.keys())
@pytest.mark.parametrize(
"fields",
[
# Single dynamic field
{"requires-python": ("python_requires", ">=3.12")},
{"author-email": ("author_email", "snoopy@peanuts.com")},
{"keywords": ("keywords", ["hello", "world"])},
{"platform": ("platforms", ["abcd"])},
# Multiple dynamic fields
{
"summary": ("description", "hello world"),
"description": ("long_description", "bla bla bla bla"),
"requires-dist": ("install_requires", ["hello-world"]),
},
],
def test_parity_with_metadata_from_pypa_wheel(tmp_path):
attrs = dict(
**EXAMPLE_BASE_INFO,
# Example with complex requirement definition
python_requires=">=3.8",
install_requires="""
packaging==23.2
more-itertools==8.8.0; extra == "other"
jaraco.text==3.7.0
importlib-resources==5.10.2; python_version<"3.8"
importlib-metadata==6.0.0 ; python_version<"3.8"
colorama>=0.4.4; sys_platform == "win32"
""",
extras_require={
"testing": """
pytest >= 6
pytest-checkdocs >= 2.4
tomli ; \\
# Using stdlib when possible
python_version < "3.11"
ini2toml[lite]>=0.9
""",
"other": [],
},
)
def test_modified_fields_marked_as_dynamic(self, file, fields, tmpdir_cwd):
# We start with a static config
Path(file).write_text(self.STATIC_CONFIG[file], encoding="utf-8")
dist = _makedist()
# ... but then we simulate the effects of a plugin modifying the distribution
for attr, value in fields.values():
# `dist` and `dist.metadata` are complicated...
# Some attributes work when set on `dist`, others on `dist.metadata`...
# Here we set in both just in case (this also avoids calling `_finalize_*`)
setattr(dist, attr, value)
setattr(dist.metadata, attr, value)
# Then we should be able to list the modified fields as Dynamic
metadata = _get_metadata(dist)
assert set(metadata.get_all("Dynamic")) == set(fields)
@pytest.mark.parametrize(
"extra_toml",
[
"# Let setuptools autofill license-files",
"license-files = ['LICENSE*', 'AUTHORS*', 'NOTICE']",
],
)
def test_license_files_dynamic(self, extra_toml, tmpdir_cwd):
# For simplicity (and for the time being) setuptools is not making
# any special handling to guarantee `License-File` is considered static.
# Instead we rely in the fact that, although suboptimal, it is OK to have
# it as dynamics, as per:
# https://github.com/pypa/setuptools/issues/4629#issuecomment-2331233677
files = {
"pyproject.toml": self.STATIC_CONFIG["pyproject.toml"].replace(
'license = "AGPL-3.0-or-later"',
f"dynamic = ['license']\n{extra_toml}",
),
"LICENSE.md": "--- mock license ---",
"NOTICE": "--- mock notice ---",
"AUTHORS.txt": "--- me ---",
}
# Sanity checks:
assert extra_toml in files["pyproject.toml"]
assert 'license = "AGPL-3.0-or-later"' not in extra_toml
jaraco.path.build(files)
dist = _makedist(license_expression="AGPL-3.0-or-later")
metadata = _get_metadata(dist)
assert set(metadata.get_all("Dynamic")) == {
'license-file',
'license-expression',
}
assert metadata.get("License-Expression") == "AGPL-3.0-or-later"
assert set(metadata.get_all("License-File")) == {
"NOTICE",
"AUTHORS.txt",
"LICENSE.md",
}
def _makedist(**attrs):
# Generate a PKG-INFO file using setuptools
dist = Distribution(attrs)
dist.parse_config_files()
return dist
with io.StringIO() as fp:
dist.metadata.write_pkg_file(fp)
pkg_info = fp.getvalue()
assert _valid_metadata(pkg_info)
def _assert_roundtrip_message(metadata: str) -> None:
"""Emulate the way wheel.bdist_wheel parses and regenerates the message,
then ensures the metadata generated by setuptools is compatible.
"""
with io.StringIO(metadata) as buffer:
msg = Parser(EmailMessage).parse(buffer)
serialization_policy = EmailPolicy(
utf8=True,
mangle_from_=False,
max_line_length=0,
)
with io.BytesIO() as buffer:
out = io.TextIOWrapper(buffer, encoding="utf-8")
Generator(out, policy=serialization_policy).flatten(msg)
out.flush()
regenerated = buffer.getvalue()
raw_metadata = bytes(metadata, "utf-8")
# Normalise newlines to avoid test errors on Windows:
raw_metadata = b"\n".join(raw_metadata.splitlines())
regenerated = b"\n".join(regenerated.splitlines())
assert regenerated == raw_metadata
def _normalize_metadata(msg: Message) -> str:
"""Allow equivalent metadata to be compared directly"""
# The main challenge regards the requirements and extras.
# Both setuptools and wheel already apply some level of normalization
# but they differ regarding which character is chosen, according to the
# following spec it should be "-":
# https://packaging.python.org/en/latest/specifications/name-normalization/
# Related issues:
# https://github.com/pypa/packaging/issues/845
# https://github.com/pypa/packaging/issues/644#issuecomment-2429813968
extras = {x.replace("_", "-"): x for x in msg.get_all("Provides-Extra", [])}
reqs = [
_normalize_req(req, extras)
for req in _reqs.parse(msg.get_all("Requires-Dist", []))
# Ensure Requires-Dist is present
expected = [
'Metadata-Version:',
'Requires-Python: >=3.8',
'Provides-Extra: other',
'Provides-Extra: testing',
'Requires-Dist: tomli; python_version < "3.11" and extra == "testing"',
'Requires-Dist: more-itertools==8.8.0; extra == "other"',
'Requires-Dist: ini2toml[lite]>=0.9; extra == "testing"',
]
del msg["Requires-Dist"]
del msg["Provides-Extra"]
for line in expected:
assert line in pkg_info
# Ensure consistent ord
for req in sorted(reqs):
msg["Requires-Dist"] = req
for extra in sorted(extras):
msg["Provides-Extra"] = extra
# Generate a METADATA file using pypa/wheel for comparison
wheel_metadata = importlib.import_module("wheel.metadata")
pkginfo_to_metadata = getattr(wheel_metadata, "pkginfo_to_metadata", None)
# TODO: Handle lack of PEP 643 implementation in pypa/wheel?
del msg["Metadata-Version"]
if pkginfo_to_metadata is None:
pytest.xfail(
"wheel.metadata.pkginfo_to_metadata is undefined, "
"(this is likely to be caused by API changes in pypa/wheel"
)
return msg.as_string()
# Generate an simplified "egg-info" dir for pypa/wheel to convert
egg_info_dir = tmp_path / "pkg.egg-info"
egg_info_dir.mkdir(parents=True)
(egg_info_dir / "PKG-INFO").write_text(pkg_info, encoding="utf-8")
write_requirements(egg_info(dist), egg_info_dir, egg_info_dir / "requires.txt")
# Get pypa/wheel generated METADATA but normalize requirements formatting
metadata_msg = pkginfo_to_metadata(egg_info_dir, egg_info_dir / "PKG-INFO")
metadata_deps = set(_reqs.parse(metadata_msg.get_all("Requires-Dist")))
metadata_extras = set(metadata_msg.get_all("Provides-Extra"))
del metadata_msg["Requires-Dist"]
del metadata_msg["Provides-Extra"]
pkg_info_msg = message_from_string(pkg_info)
pkg_info_deps = set(_reqs.parse(pkg_info_msg.get_all("Requires-Dist")))
pkg_info_extras = set(pkg_info_msg.get_all("Provides-Extra"))
del pkg_info_msg["Requires-Dist"]
del pkg_info_msg["Provides-Extra"]
def _normalize_req(req: Requirement, extras: dict[str, str]) -> str:
"""Allow equivalent requirement objects to be compared directly"""
as_str = str(req).replace(req.name, req.name.replace("_", "-"))
for norm, orig in extras.items():
as_str = as_str.replace(orig, norm)
return as_str
# Compare setuptools PKG-INFO x pypa/wheel METADATA
assert metadata_msg.as_string() == pkg_info_msg.as_string()
assert metadata_deps == pkg_info_deps
assert metadata_extras == pkg_info_extras
def _get_pkginfo(dist: Distribution):
with io.StringIO() as fp:
dist.metadata.write_pkg_file(fp)
return fp.getvalue()
def _get_metadata(dist: Distribution | None = None):
return message_from_string(_get_pkginfo(dist or _makedist()))
def _valid_metadata(text: str) -> bool:
metadata = Metadata.from_email(text, validate=True) # can raise exceptions
return metadata is not None
"""develop tests"""
import os
import pathlib
import platform

@@ -11,2 +12,4 @@ import subprocess

from setuptools._path import paths_on_pythonpath
from setuptools.command.develop import develop
from setuptools.dist import Distribution

@@ -52,2 +55,63 @@ from . import contexts, namespaces

class TestDevelop:
in_virtualenv = hasattr(sys, 'real_prefix')
in_venv = hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix
def test_console_scripts(self, tmpdir):
"""
Test that console scripts are installed and that they reference
only the project by name and not the current version.
"""
pytest.skip(
"TODO: needs a fixture to cause 'develop' "
"to be invoked without mutating environment."
)
settings = dict(
name='foo',
packages=['foo'],
version='0.0',
entry_points={
'console_scripts': [
'foocmd = foo:foo',
],
},
)
dist = Distribution(settings)
dist.script_name = 'setup.py'
cmd = develop(dist)
cmd.ensure_finalized()
cmd.install_dir = tmpdir
cmd.run()
# assert '0.0' not in foocmd_text
@pytest.mark.xfail(reason="legacy behavior retained for compatibility #4167")
def test_egg_link_filename(self):
settings = dict(
name='Foo $$$ Bar_baz-bing',
)
dist = Distribution(settings)
cmd = develop(dist)
cmd.ensure_finalized()
link = pathlib.Path(cmd.egg_link)
assert link.suffix == '.egg-link'
assert link.stem == 'Foo_Bar_baz_bing'
class TestResolver:
"""
TODO: These tests were written with a minimal understanding
of what _resolve_setup_path is intending to do. Come up with
more meaningful cases that look like real-world scenarios.
"""
def test_resolve_setup_path_cwd(self):
assert develop._resolve_setup_path('.', '.', '.') == '.'
def test_resolve_setup_path_one_dir(self):
assert develop._resolve_setup_path('pkgs', '.', 'pkgs') == '../'
def test_resolve_setup_path_one_dir_trailing_slash(self):
assert develop._resolve_setup_path('pkgs/', '.', 'pkgs') == '../'
class TestNamespaces:

@@ -75,3 +139,2 @@ @staticmethod

)
@pytest.mark.uses_network
def test_namespace_package_importable(self, tmpdir):

@@ -78,0 +141,0 @@ """

@@ -12,2 +12,3 @@ """Test .dist-info style distributions."""

import pkg_resources
from setuptools.archive_util import unpack_archive

@@ -21,2 +22,64 @@

class TestDistInfo:
metadata_base = DALS(
"""
Metadata-Version: 1.2
Requires-Dist: splort (==4)
Provides-Extra: baz
Requires-Dist: quux (>=1.1); extra == 'baz'
"""
)
@classmethod
def build_metadata(cls, **kwargs):
lines = ('{key}: {value}\n'.format(**locals()) for key, value in kwargs.items())
return cls.metadata_base + ''.join(lines)
@pytest.fixture
def metadata(self, tmpdir):
dist_info_name = 'VersionedDistribution-2.718.dist-info'
versioned = tmpdir / dist_info_name
versioned.mkdir()
filename = versioned / 'METADATA'
content = self.build_metadata(
Name='VersionedDistribution',
)
filename.write_text(content, encoding='utf-8')
dist_info_name = 'UnversionedDistribution.dist-info'
unversioned = tmpdir / dist_info_name
unversioned.mkdir()
filename = unversioned / 'METADATA'
content = self.build_metadata(
Name='UnversionedDistribution',
Version='0.3',
)
filename.write_text(content, encoding='utf-8')
return str(tmpdir)
def test_distinfo(self, metadata):
dists = dict(
(d.project_name, d) for d in pkg_resources.find_distributions(metadata)
)
assert len(dists) == 2, dists
unversioned = dists['UnversionedDistribution']
versioned = dists['VersionedDistribution']
assert versioned.version == '2.718' # from filename
assert unversioned.version == '0.3' # from METADATA
def test_conditional_dependencies(self, metadata):
specs = 'splort==4', 'quux>=1.1'
requires = list(map(pkg_resources.Requirement.parse, specs))
for d in pkg_resources.find_distributions(metadata):
assert d.requires() == requires[:1]
assert d.requires(extras=('baz',)) == [
requires[0],
pkg_resources.Requirement.parse('quux>=1.1;extra=="baz"'),
]
assert d.extras == ['baz']
def test_invalid_version(self, tmp_path):

@@ -111,3 +174,3 @@ """

@pytest.mark.parametrize("version", ["0.42.13"])
@pytest.mark.parametrize(("suffix", "cfg"), EGG_INFO_OPTS)
@pytest.mark.parametrize("suffix, cfg", EGG_INFO_OPTS)
def test_dist_info_is_the_same_as_in_wheel(

@@ -114,0 +177,0 @@ self, name, version, tmp_path, suffix, cfg

@@ -11,3 +11,3 @@ import os

from .fixtures import make_trivial_sdist
from .test_easy_install import make_trivial_sdist
from .test_find_packages import ensure_files

@@ -28,3 +28,3 @@ from .textwrap import DALS

dist_dir = index.mkdir(distname)
dist_sdist = f'{distname}-{version}.tar.gz'
dist_sdist = '%s-%s.tar.gz' % (distname, version)
make_trivial_sdist(str(dist_dir.join(dist_sdist)), distname, version)

@@ -61,3 +61,3 @@ with dist_dir.join('index.html').open('w') as fp:

resolved_dists = [dist.fetch_build_egg(r) for r in reqs]
assert [dist.name for dist in resolved_dists if dist] == reqs
assert [dist.key for dist in resolved_dists if dist] == reqs

@@ -135,3 +135,3 @@

@pytest.mark.parametrize(('package_data', 'expected_message'), CHECK_PACKAGE_DATA_TESTS)
@pytest.mark.parametrize('package_data, expected_message', CHECK_PACKAGE_DATA_TESTS)
def test_check_package_data(package_data, expected_message):

@@ -145,2 +145,3 @@ if expected_message is None:

@pytest.mark.xfail(reason="#4745")
def test_check_specifier():

@@ -152,8 +153,4 @@ # valid specifier value

# invalid specifier value
attrs = {'name': 'foo', 'python_requires': ['>=3.0', '!=3.1']}
dist = Distribution(attrs)
check_specifier(dist, attrs, attrs['python_requires'])
# invalid specifier value
attrs = {'name': 'foo', 'python_requires': '>=invalid-version'}
with pytest.raises(DistutilsSetupError):

@@ -169,3 +166,3 @@ dist = Distribution(attrs)

@pytest.mark.parametrize(
('dist_name', 'py_module'),
"dist_name, py_module",
[

@@ -201,3 +198,3 @@ ("my.pkg", "my_pkg"),

@pytest.mark.parametrize(
('dist_name', 'package_dir', 'package_files', 'packages'),
"dist_name, package_dir, package_files, packages",
[

@@ -256,3 +253,3 @@ ("my.pkg", None, ["my_pkg/__init__.py", "my_pkg/mod.py"], ["my_pkg"]),

@pytest.mark.parametrize(
('dist_name', 'package_dir', 'package_files'),
"dist_name, package_dir, package_files",
[

@@ -259,0 +256,0 @@ ("my.pkg.nested", None, ["my/pkg/nested/__init__.py"]),

@@ -119,3 +119,3 @@ import os

@pytest.mark.parametrize(
('distutils_version', 'imported_module'),
"distutils_version, imported_module",
[

@@ -179,3 +179,3 @@ pytest.param("stdlib", "dir_util", marks=skip_without_stdlib_distutils),

@pytest.mark.parametrize(
('distutils_version', 'imported_module'),
"distutils_version, imported_module",
[

@@ -182,0 +182,0 @@ ("local", "distutils"),

@@ -24,2 +24,3 @@ from __future__ import annotations

from setuptools.command.editable_wheel import (
_DebuggingTips,
_encode_pth,

@@ -1071,16 +1072,41 @@ _find_namespaces,

@pytest.mark.uses_network
def test_pbr_integration(pbr_package, venv, editable_opts):
def test_pbr_integration(tmp_path, venv, editable_opts):
"""Ensure editable installs work with pbr, issue #3500"""
cmd = [
'python',
'-m',
'pip',
'-v',
'install',
'--editable',
pbr_package,
*editable_opts,
]
venv.run(cmd, stderr=subprocess.STDOUT)
files = {
"pyproject.toml": dedent(
"""\
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
"""
),
"setup.py": dedent(
"""\
__import__('setuptools').setup(
pbr=True,
setup_requires=["pbr"],
)
"""
),
"setup.cfg": dedent(
"""\
[metadata]
name = mypkg
[files]
packages =
mypkg
"""
),
"mypkg": {
"__init__.py": "",
"hello.py": "print('Hello world!')",
},
"other": {"test.txt": "Another file in here."},
}
venv.run(["python", "-m", "pip", "install", "pbr"])
with contexts.environment(PBR_VERSION="0.42"):
install_project("mypkg", venv, tmp_path, files, *editable_opts)
out = venv.run(["python", "-c", "import mypkg.hello"])

@@ -1206,5 +1232,5 @@ assert "Hello world!" in out

with pytest.raises(SimulatedErr) as ctx:
expected_msg = "following steps are recommended to help debug"
with pytest.raises(SimulatedErr), pytest.warns(_DebuggingTips, match=expected_msg):
cmd.run()
assert any('debugging-tips' in note for note in ctx.value.__notes__)

@@ -1255,3 +1281,3 @@

def assert_link_to(file: Path, other: Path) -> None:
def assert_link_to(file: Path, other: Path):
if file.is_symlink():

@@ -1258,0 +1284,0 @@ assert str(file.resolve()) == str(other.resolve())

@@ -41,4 +41,5 @@ from __future__ import annotations

[egg_info]
egg-base = {egg-base}
""".format(**env.paths)
egg-base = %(egg-base)s
"""
% env.paths
)

@@ -263,5 +264,9 @@ }

mismatch_marker = f"python_version<'{sys.version_info[0]}'"
mismatch_marker = "python_version<'{this_ver}'".format(
this_ver=sys.version_info[0],
)
# Alternate equivalent syntax.
mismatch_marker_alternate = f'python_version < "{sys.version_info[0]}"'
mismatch_marker_alternate = 'python_version < "{this_ver}"'.format(
this_ver=sys.version_info[0],
)
invalid_marker = "<=>++"

@@ -305,8 +310,3 @@

return pytest.mark.parametrize(
(
"requires",
"use_setup_cfg",
"expected_requires",
"install_cmd_kwargs",
),
'requires,use_setup_cfg,expected_requires,install_cmd_kwargs',
argvalues,

@@ -518,3 +518,3 @@ ids=idlist,

)
environment.run_setup_py(
code, data = environment.run_setup_py(
cmd=['egg_info'],

@@ -529,3 +529,3 @@ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),

assert 'Provides-Extra: foobar' in pkg_info_lines
assert 'Metadata-Version: 2.4' in pkg_info_lines
assert 'Metadata-Version: 2.1' in pkg_info_lines

@@ -551,3 +551,3 @@ def test_doesnt_provides_extra(self, tmpdir_cwd, env):

@pytest.mark.parametrize(
('files', 'license_in_sources'),
"files, license_in_sources",
[

@@ -636,3 +636,3 @@ (

@pytest.mark.parametrize(
('files', 'incl_licenses', 'excl_licenses'),
"files, incl_licenses, excl_licenses",
[

@@ -825,18 +825,2 @@ (

),
pytest.param(
{
'setup.cfg': DALS(
"""
[metadata]
license_files = **/LICENSE
"""
),
'LICENSE': "ABC license",
'LICENSE-OTHER': "Don't include",
'vendor': {'LICENSE': "Vendor license"},
},
['LICENSE', 'vendor/LICENSE'],
['LICENSE-OTHER'],
id="recursive_glob",
),
],

@@ -866,3 +850,3 @@ )

@pytest.mark.parametrize(
('files', 'incl_licenses', 'excl_licenses'),
"files, incl_licenses, excl_licenses",
[

@@ -1060,3 +1044,2 @@ (

LICENSE*
**/LICENSE
"""

@@ -1068,3 +1051,2 @@ ),

"IGNORE": "not include",
"vendor": {'LICENSE': "Vendor license"},
})

@@ -1085,7 +1067,5 @@

# Also assert that order from license_files is keeped
assert len(license_file_lines) == 4
assert "License-File: NOTICE" == license_file_lines[0]
assert "License-File: LICENSE-ABC" in license_file_lines[1:]
assert "License-File: LICENSE-XYZ" in license_file_lines[1:]
assert "License-File: vendor/LICENSE" in license_file_lines[3]

@@ -1095,3 +1075,3 @@ def test_metadata_version(self, tmpdir_cwd, env):

self._setup_script_with_requires("")
environment.run_setup_py(
code, data = environment.run_setup_py(
cmd=['egg_info'],

@@ -1105,3 +1085,3 @@ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),

# Update metadata version if changed
assert self._extract_mv_version(pkg_info_lines) == (2, 4)
assert self._extract_mv_version(pkg_info_lines) == (2, 1)

@@ -1122,3 +1102,3 @@ def test_long_description_content_type(self, tmpdir_cwd, env):

)
environment.run_setup_py(
code, data = environment.run_setup_py(
cmd=['egg_info'],

@@ -1134,3 +1114,3 @@ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),

assert expected_line in pkg_info_lines
assert 'Metadata-Version: 2.4' in pkg_info_lines
assert 'Metadata-Version: 2.1' in pkg_info_lines

@@ -1146,3 +1126,3 @@ def test_long_description(self, tmpdir_cwd, env):

)
environment.run_setup_py(
code, data = environment.run_setup_py(
cmd=['egg_info'],

@@ -1155,3 +1135,3 @@ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),

pkg_info_lines = fp.read().split('\n')
assert 'Metadata-Version: 2.4' in pkg_info_lines
assert 'Metadata-Version: 2.1' in pkg_info_lines
assert '' == pkg_info_lines[-1] # last line should be empty

@@ -1179,3 +1159,3 @@ long_desc_lines = pkg_info_lines[pkg_info_lines.index('') :]

)
environment.run_setup_py(
code, data = environment.run_setup_py(
cmd=['egg_info'],

@@ -1198,3 +1178,3 @@ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),

self._setup_script_with_requires("license='MIT',")
environment.run_setup_py(
code, data = environment.run_setup_py(
cmd=['egg_info'],

@@ -1214,3 +1194,3 @@ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),

)
environment.run_setup_py(
code, data = environment.run_setup_py(
cmd=['egg_info'],

@@ -1233,3 +1213,3 @@ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),

)
environment.run_setup_py(
code, data = environment.run_setup_py(
cmd=['egg_info'],

@@ -1312,7 +1292,6 @@ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),

cmd = dist.get_command_obj("egg_info")
expected_msg = r"(Invalid object reference|Problems to parse)"
with pytest.raises((errors.OptionError, ValueError), match=expected_msg) as ex:
expected_msg = r"Problems to parse .*invalid-identifier.*"
with pytest.raises(errors.OptionError, match=expected_msg) as ex:
write_entries(cmd, "entry_points", "entry_points.txt")
assert "ensure entry-point follows the spec" in ex.value.args[0]
assert "invalid-identifier" in str(ex.value)

@@ -1319,0 +1298,0 @@ def test_valid_entry_point(self, tmpdir_cwd, env):

@@ -8,3 +8,3 @@ import pytest

@pytest.mark.parametrize(
('tree', 'pattern', 'matches'),
'tree, pattern, matches',
(

@@ -11,0 +11,0 @@ ('', b'', []),

@@ -41,3 +41,3 @@ """install_scripts tests"""

"""
expected = f'#!{self.unix_exe}\n'
expected = '#!%s\n' % self.unix_exe
monkeypatch.setattr('sys.executable', self.unix_exe)

@@ -56,3 +56,3 @@ with tmpdir.as_cwd():

"""
expected = f'#!"{self.win32_exe}"\n'
expected = '#!"%s"\n' % self.win32_exe
monkeypatch.setattr('sys.executable', self.win32_exe)

@@ -72,3 +72,3 @@ with tmpdir.as_cwd():

"""
expected = f'#!{self.unix_spaces_exe}\n'
expected = '#!%s\n' % self.unix_spaces_exe
with tmpdir.as_cwd():

@@ -87,3 +87,3 @@ self._run_install_scripts(str(tmpdir), self.unix_spaces_exe)

"""
expected = f'#!"{self.win32_exe}"\n'
expected = '#!"%s"\n' % self.win32_exe
with tmpdir.as_cwd():

@@ -90,0 +90,0 @@ self._run_install_scripts(str(tmpdir), '"' + self.win32_exe + '"')

@@ -22,3 +22,3 @@ import functools

@pytest.mark.parametrize(
('flag', 'expected_level'), [("--dry-run", "INFO"), ("--verbose", "DEBUG")]
"flag, expected_level", [("--dry-run", "INFO"), ("--verbose", "DEBUG")]
)

@@ -25,0 +25,0 @@ def test_verbosity_level(tmp_path, monkeypatch, flag, expected_level):

@@ -37,7 +37,10 @@ """sdist tests"""

SETUP_PY = f"""\
SETUP_PY = (
"""\
from setuptools import setup
setup(**{SETUP_ATTRS!r})
setup(**%r)
"""
% SETUP_ATTRS
)

@@ -370,3 +373,3 @@

file = os.path.join(self.temp_dir, file)
dirname, _basename = os.path.split(file)
dirname, basename = os.path.split(file)
os.makedirs(dirname, exist_ok=True)

@@ -373,0 +376,0 @@ touch(file)

@@ -40,7 +40,10 @@ """sdist tests"""

SETUP_PY = f"""\
SETUP_PY = (
"""\
from setuptools import setup
setup(**{SETUP_ATTRS!r})
setup(**%r)
"""
% SETUP_ATTRS
)

@@ -712,3 +715,3 @@ EXTENSION = Extension(

readme = "USAGE.rst"
license-files = ["DOWHATYOUWANT"]
license = {file = "DOWHATYOUWANT"}
dynamic = ["version"]

@@ -722,11 +725,2 @@ [tool.setuptools.dynamic]

readme = "USAGE.rst"
license-files = ["DOWHATYOUWANT"]
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {file = "src/VERSION.txt"}
""",
"pyproject.toml - deprecated license table with file entry": """
[project]
name = "testing"
readme = "USAGE.rst"
license = {file = "DOWHATYOUWANT"}

@@ -740,5 +734,2 @@ dynamic = ["version"]

@pytest.mark.parametrize("config", _EXAMPLE_DIRECTIVES.keys())
@pytest.mark.filterwarnings(
"ignore:.project.license. as a TOML table is deprecated"
)
def test_add_files_referenced_by_config_directives(self, source_dir, config):

@@ -745,0 +736,0 @@ config_file, _, _ = config.partition(" - ")

@@ -24,3 +24,3 @@ """Tests for the 'setuptools' package"""

def isolated_dir(tmpdir_cwd):
return
yield

@@ -78,3 +78,3 @@

dep.find_module('setuptools.non-existent')
f, _p, _i = dep.find_module('setuptools.tests')
f, p, i = dep.find_module('setuptools.tests')
f.close()

@@ -262,4 +262,3 @@

@pytest.mark.usefixtures("can_symlink")
def test_findall_missing_symlink(tmpdir):
def test_findall_missing_symlink(tmpdir, can_symlink):
with tmpdir.as_cwd():

@@ -266,0 +265,0 @@ os.symlink('foo', 'bar')

@@ -10,6 +10,6 @@ """wheel tests"""

import pathlib
import shutil
import stat
import subprocess
import sys
import sysconfig
import zipfile

@@ -21,4 +21,5 @@ from typing import Any

from packaging.tags import parse_tag
from packaging.utils import canonicalize_name
from setuptools._importlib import metadata
from pkg_resources import PY_MAJOR, Distribution, PathMetadata
from setuptools.wheel import Wheel

@@ -144,3 +145,3 @@

x.format(
py_version=sysconfig.get_python_version(),
py_version=PY_MAJOR,
platform=get_platform(),

@@ -164,11 +165,13 @@ shlib_ext=get_config_var('EXT_SUFFIX') or get_config_var('SO'),

(dist,) = metadata.Distribution.discover(path=[egg_path])
metadata = PathMetadata(egg_path, os.path.join(egg_path, 'EGG-INFO'))
dist = Distribution.from_filename(egg_path, metadata=metadata)
assert dist.project_name == project_name
assert dist.version == version
if requires_txt is None:
assert not dist.has_metadata('requires.txt')
else:
# Order must match to ensure reproducibility.
assert requires_txt == dist.get_metadata('requires.txt').lstrip()
# pyright is nitpicky; fine to assume dist.metadata.__getitem__ will fail or return None
# (https://github.com/pypa/setuptools/pull/5006#issuecomment-2894774288)
assert dist.metadata['Name'] == project_name # pyright: ignore # noqa: PGH003
assert dist.metadata['Version'] == version # pyright: ignore # noqa: PGH003
assert dist.read_text('requires.txt') == requires_txt
class Record:

@@ -180,3 +183,3 @@ def __init__(self, id, **kwargs):

def __repr__(self) -> str:
return f'{self._id}(**{self._fields!r})'
return '%s(**%r)' % (self._id, self._fields)

@@ -372,6 +375,7 @@

id='requires2',
install_requires=f"""
install_requires="""
bar
foo<=2.0; {sys.platform!r} in sys_platform
""",
foo<=2.0; %r in sys_platform
"""
% sys.platform,
requires_txt=DALS(

@@ -386,5 +390,6 @@ """

id='requires3',
install_requires=f"""
bar; {sys.platform!r} != sys_platform
""",
install_requires="""
bar; %r != sys_platform
"""
% sys.platform,
),

@@ -411,6 +416,5 @@ dict(

extras_require={
'extra': f'foobar; {sys.platform!r} != sys_platform',
'extra': 'foobar; %r != sys_platform' % sys.platform,
},
requires_txt='\n'
+ DALS(
requires_txt=DALS(
"""

@@ -571,13 +575,10 @@ [extra]

setup_kwargs = params.get('setup_kwargs', {})
with (
build_wheel(
name=project_name,
version=version,
install_requires=install_requires,
extras_require=extras_require,
extra_file_defs=file_defs,
**setup_kwargs,
) as filename,
tempdir() as install_dir,
):
with build_wheel(
name=project_name,
version=version,
install_requires=install_requires,
extras_require=extras_require,
extra_file_defs=file_defs,
**setup_kwargs,
) as filename, tempdir() as install_dir:
_check_wheel_install(

@@ -588,6 +589,25 @@ filename, install_dir, install_tree, project_name, version, requires_txt

def test_wheel_install_pep_503():
project_name = 'Foo_Bar' # PEP 503 canonicalized name is "foo-bar"
version = '1.0'
with build_wheel(
name=project_name,
version=version,
) as filename, tempdir() as install_dir:
new_filename = filename.replace(project_name, canonicalize_name(project_name))
shutil.move(filename, new_filename)
_check_wheel_install(
new_filename,
install_dir,
None,
canonicalize_name(project_name),
version,
None,
)
def test_wheel_no_dist_dir():
project_name = 'nodistinfo'
version = '1.0'
wheel_name = f'{project_name}-{version}-py2.py3-none-any.whl'
wheel_name = '{0}-{1}-py2.py3-none-any.whl'.format(project_name, version)
with tempdir() as source_dir:

@@ -680,13 +700,10 @@ wheel_path = os.path.join(source_dir, wheel_name)

with (
build_wheel(
name=project_name,
version=version,
install_requires=[],
extras_require={},
extra_file_defs=file_defs,
**setup_kwargs,
) as filename,
tempdir() as install_dir,
):
with build_wheel(
name=project_name,
version=version,
install_requires=[],
extras_require={},
extra_file_defs=file_defs,
**setup_kwargs,
) as filename, tempdir() as install_dir:
_check_wheel_install(

@@ -693,0 +710,0 @@ filename, install_dir, install_tree, project_name, version, None

@@ -23,3 +23,4 @@ """

from setuptools._importlib import resources
import pkg_resources
from setuptools.command.easy_install import nt_quote_arg

@@ -32,3 +33,3 @@ pytestmark = pytest.mark.skipif(sys.platform != 'win32', reason="Windows only")

def prep_script(cls, template):
python_exe = subprocess.list2cmdline([sys.executable])
python_exe = nt_quote_arg(sys.executable)
return template % locals()

@@ -53,3 +54,3 @@

with (tmpdir / cls.wrapper_name).open('wb') as f:
w = resources.files('setuptools').joinpath(cls.wrapper_source).read_bytes()
w = pkg_resources.resource_string('setuptools', cls.wrapper_source)
f.write(w)

@@ -62,5 +63,5 @@

if platform.machine() == "ARM64":
return f"{prefix}-arm64.exe"
return "{}-arm64.exe".format(prefix)
else:
return f"{prefix}-32.exe"
return "{}-32.exe".format(prefix)

@@ -122,3 +123,3 @@

)
stdout, _stderr = proc.communicate('hello\nworld\n')
stdout, stderr = proc.communicate('hello\nworld\n')
actual = stdout.replace('\r\n', '\n')

@@ -160,3 +161,3 @@ expected = textwrap.dedent(

)
stdout, _stderr = proc.communicate('hello\nworld\n')
stdout, stderr = proc.communicate('hello\nworld\n')
actual = stdout.replace('\r\n', '\n')

@@ -209,3 +210,3 @@ expected = textwrap.dedent(

)
stdout, _stderr = proc.communicate()
stdout, stderr = proc.communicate()
actual = stdout.replace('\r\n', '\n')

@@ -212,0 +213,0 @@ expected = textwrap.dedent(

import sys
import unicodedata
from configparser import RawConfigParser
from configparser import ConfigParser

@@ -68,6 +68,6 @@ from .compat import py39

def _cfg_read_utf8_with_fallback(
cfg: RawConfigParser, file: str, fallback_encoding=py39.LOCALE_ENCODING
cfg: ConfigParser, file: str, fallback_encoding=py39.LOCALE_ENCODING
) -> None:
"""Same idea as :func:`_read_utf8_with_fallback`, but for the
:meth:`RawConfigParser.read` method.
:meth:`ConfigParser.read` method.

@@ -90,3 +90,3 @@ This method may call ``cfg.clear()``.

_DETAILS = """
Fallback behavior for UTF-8 is considered **deprecated** and future versions of
Fallback behaviour for UTF-8 is considered **deprecated** and future versions of
`setuptools` may not implement it.

@@ -93,0 +93,0 @@

@@ -15,3 +15,3 @@ """Provide basic warnings used by setuptools modules.

from textwrap import indent
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Tuple

@@ -21,3 +21,3 @@ if TYPE_CHECKING:

_DueDate: TypeAlias = tuple[int, int, int] # time tuple
_DueDate: TypeAlias = Tuple[int, int, int] # time tuple
_INDENT = 8 * " "

@@ -24,0 +24,0 @@ _TEMPLATE = f"""{80 * '*'}\n{{details}}\n{80 * '*'}"""

@@ -12,3 +12,2 @@ """Wheels support."""

from packaging.requirements import Requirement
from packaging.tags import sys_tags

@@ -22,4 +21,2 @@ from packaging.utils import canonicalize_name

from ._discovery import extras_from_deps
from ._importlib import metadata
from .unicode_utils import _read_utf8_with_fallback

@@ -39,3 +36,3 @@

@functools.cache
@functools.lru_cache(maxsize=None)
def _get_supported_tags():

@@ -48,3 +45,3 @@ # We calculate the supported tags only once, otherwise calling

def unpack(src_dir, dst_dir) -> None:
def unpack(src_dir, dst_dir):
"""Move everything under `src_dir` to `dst_dir`, and delete the former."""

@@ -86,6 +83,6 @@ for dirpath, dirnames, filenames in os.walk(src_dir):

class Wheel:
def __init__(self, filename) -> None:
def __init__(self, filename):
match = WHEEL_NAME(os.path.basename(filename))
if match is None:
raise ValueError(f'invalid wheel name: {filename!r}')
raise ValueError('invalid wheel name: %r' % filename)
self.filename = filename

@@ -127,3 +124,3 @@ for k, v in match.groupdict().items():

def install_as_egg(self, destination_eggdir) -> None:
def install_as_egg(self, destination_eggdir):
"""Install wheel as an egg directory."""

@@ -134,5 +131,5 @@ with zipfile.ZipFile(self.filename) as zf:

def _install_as_egg(self, destination_eggdir, zf):
dist_basename = f'{self.project_name}-{self.version}'
dist_basename = '%s-%s' % (self.project_name, self.version)
dist_info = self.get_dist_info(zf)
dist_data = f'{dist_basename}.data'
dist_data = '%s.data' % dist_basename
egg_info = os.path.join(destination_eggdir, 'EGG-INFO')

@@ -146,2 +143,4 @@

def _convert_metadata(zf, destination_eggdir, dist_info, egg_info):
import pkg_resources
def get_metadata(name):

@@ -157,9 +156,29 @@ with zf.open(posixpath.join(dist_info, name)) as fp:

if not wheel_v1:
raise ValueError(f'unsupported wheel format version: {wheel_version}')
raise ValueError('unsupported wheel format version: %s' % wheel_version)
# Extract to target directory.
_unpack_zipfile_obj(zf, destination_eggdir)
# Convert metadata.
dist_info = os.path.join(destination_eggdir, dist_info)
install_requires, extras_require = Wheel._convert_requires(
destination_eggdir, dist_info
dist = pkg_resources.Distribution.from_location(
destination_eggdir,
dist_info,
metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info),
)
# Note: Evaluate and strip markers now,
# as it's difficult to convert back from the syntax:
# foobar; "linux" in sys_platform and extra == 'test'
def raw_req(req):
req.marker = None
return str(req)
install_requires = list(map(raw_req, dist.requires()))
extras_require = {
extra: [
req
for req in map(raw_req, dist.requires((extra,)))
if req not in install_requires
]
for extra in dist.extras
}
os.rename(dist_info, egg_info)

@@ -184,46 +203,2 @@ os.rename(

@staticmethod
def _convert_requires(destination_eggdir, dist_info):
md = metadata.Distribution.at(dist_info).metadata
deps = md.get_all('Requires-Dist') or []
reqs = list(map(Requirement, deps))
extras = extras_from_deps(deps)
# Note: Evaluate and strip markers now,
# as it's difficult to convert back from the syntax:
# foobar; "linux" in sys_platform and extra == 'test'
def raw_req(req):
req = Requirement(str(req))
req.marker = None
return str(req)
def eval(req, **env):
return not req.marker or req.marker.evaluate(env)
def for_extra(req):
try:
markers = req.marker._markers
except AttributeError:
markers = ()
return set(
marker[2].value
for marker in markers
if isinstance(marker, tuple) and marker[0].value == 'extra'
)
install_requires = list(
map(raw_req, filter(eval, itertools.filterfalse(for_extra, reqs)))
)
extras_require = {
extra: list(
map(
raw_req,
(req for req in reqs if for_extra(req) and eval(req, extra=extra)),
)
)
for extra in extras
}
return install_requires, extras_require
@staticmethod
def _move_data_entries(destination_eggdir, dist_data):

@@ -230,0 +205,0 @@ """Move data entries to their correct location."""

@@ -57,3 +57,3 @@ """

def resolve_platform(platform: str) -> str:
def resolve_platform(platform: str):
if platform in ["Win32", "x64"]:

@@ -64,3 +64,3 @@ return platform[-2:]

def get_executable_name(name, platform: str) -> str:
def get_executable_name(name, platform: str):
return f"{name}-{resolve_platform(platform)}"

@@ -67,0 +67,0 @@

@@ -6,4 +6,4 @@ from __future__ import annotations

import sys
from collections.abc import Iterable
from pathlib import Path
from typing import Iterable

@@ -10,0 +10,0 @@

@@ -57,2 +57,4 @@ [testenv]

changedir = docs
deps =
importlib_resources < 6 # twisted/towncrier#528 (waiting for release)
commands =

@@ -69,2 +71,3 @@ python -m sphinx -W --keep-going . {toxinidir}/build/html

jaraco.develop >= 7.23
importlib_resources < 6 # twisted/towncrier#528 (waiting for release)
pass_env = *

@@ -80,3 +83,3 @@ commands =

# workaround for pypa/pyproject-hooks#192
pyproject-hooks!=1.1
pyproject-hooks<1.1
uv

@@ -89,3 +92,3 @@ commands =

deps =
validate-pyproject[all]==0.23
validate-pyproject[all]==0.19
commands =

@@ -92,0 +95,0 @@ python -m tools.generate_validation_code

Supported Interfaces
====================
Setuptools is a complicated library with many interface surfaces and challenges. In addition to its primary purpose as a packaging build backend, Setuptools also has historically served as a standalone builder, installer, uploader, metadata provider, and more. Additionally, because it's implemented as a Python library, its entire functionality is incidentally exposed as a library.
In addition to operating as a library, because newer versions of Setuptools are often used to build older (sometimes decades-old) packages, it has a high burden of stability.
In order to have the ability to make sensible changes to the project, downstream developers and consumers should avoid depending on internal implementation details of the library and should rely only on the supported interfaces:
- *Tier 1*: APIs required by modern PyPA packaging standards (:pep:`517`, :pep:`660`) and Documented APIs for customising build behavior or creating plugins (:doc:`/userguide/extension`, :doc:`/references/keywords`):
These APIs are expected to be extremely stable and have deprecation notices and periods prior to backward incompatible changes or removals.
Please note that *functional and integration tests* capture specific behaviors and expectations about how the library and system is intended to work for outside users;
and *code comments and docstrings* (including in tests) may provide specific protections to limit the changes to behaviors on which a downstream consumer can rely.
- *Tier 2*: Documented ``distutils`` APIs:
``setuptools`` strives to honor the interfaces provided by ``distutils`` and
will coordinate with the ``pypa/distutils`` repository so that the
appropriate deprecation notices are issued.
In principle, these are documented in :doc:`/deprecated/distutils/apiref`.
Please note however that when a suitable replacement is available or advised,
the existing ``distutils`` API is considered deprecated and should not be used
(see :pep:`632#migration-advice` and :doc:`/deprecated/distutils-legacy`).
Depending on other behaviors is risky and subject to future breakage. If a project wishes to consider using interfaces that aren't covered above, consider requesting those interfaces to be added prior to depending on them (perhaps through a pull request implementing the change and relevant regression tests).
Please check further information about deprecated and unsupported behaviors in :doc:`/deprecated/index`.
Support Policy Exceptions
-------------------------
Behaviors and interfaces explicitly documented/advertised as deprecated,
or that :obj:`issue deprecation warnings <warnings.warn>`
will be supported up to the end of the announced deprecation period.
However there are a few circumstances in which the Setuptools' maintainers
reserve the right of speeding up the deprecation cycle and shortening deprecation periods:
1. When security vulnerabilities are identified in specific code paths and the
reworking existing APIs is not viable.
2. When standards in the Python packaging ecosystem externally drive non-backward
compatible changes in the code base.
3. When changes in behavior are externally driven by 3rd-party dependencies
and code maintained outside of the ``pypa/setuptools`` repository.
Note that these are exceptional circumstances and that the project will
carefully attempt to find alternatives before resorting to unscheduled removals.
What to do when deprecation periods are undefined?
--------------------------------------------------
In some cases it is difficult to define how long Setuptools will take
to remove certain features, behaviors or APIs.
For example, it may be complicated to assess how wide-spread the usage
of a certain feature is in the ecosystem.
Therefore, Setuptools may start to issue deprecation warnings without a clear due date.
This occurs because we want to notify consumers about upcoming breaking
changes as soon as possible so that they can start working in migration plans.
This does not mean that users should treat this deprecation as low priority or
interpret the lack of due date as a signal that a breaking change will never happen.
The advised course of action is for users to create a migration plan
as soon as they have identified to be subject to a Setuptools deprecation.
Setuptools may introduce relatively short deprecation periods (e.g. 6 months)
when a deprecation warning has already been issued for a long period without a
explicit due date.
How to stay on top of upcoming deprecations?
--------------------------------------------
It is a good idea to employ an automated test suite with relatively good
coverage in your project and keep an eye on the logs.
You can also automate this process by forwarding the standard output/error
streams to a log file and using heuristics to identify deprecations
(e.g. by searching for the word ``deprecation`` or ``deprecated``).
You may need to increase the level of verbosity of your output as
some tools may hide log messages by default (e.g. via ``pip -vv install ...``).
Additionally, if you are supporting a project that depends on Setuptools,
you can implement a CI workflow that leverages
:external+python:ref:`Python warning filters <warning-filter>`
to improve the visibility of warnings.
This workflow can be comprised, for example, of 3 iterative steps that require
developers to acknowledge the deprecation warnings:
1. Leverage Python Warning's Filter to transform warnings into exceptions during automated tests.
2. Devise a migration plan:
- It is a good idea to track deprecations as if they were issues,
and apply project management techniques to monitor the progress in handling them.
- Determine which parts of your code are affected and understand
the changes required to eliminate the warnings.
3. Modify the warning's filter you are using in the CI to not fail
with the newly identified exceptions (e.g. by using the ``default`` action
with a specific category or regular expression for the warning message).
This can be done globally for the whole test suite or locally in a
test-by-test basis.
Test tools like :pypi:`pytest` offer CLI and configuration options
to facilitate controlling the warning's filter (see :external+pytest:doc:`how-to/capture-warnings`).
Note that there are many ways to incorporate such workflow in your CI.
For example, if you have enough deployment resources and consider
deprecation warning management to be a day-to-day development test
you can set the warning's filter directly on your main CI loop.
On the other hand if you have critical timelines and cannot afford CI jobs
occasionally failing to flag maintenance, you can consider scheduling a
periodic CI run separated from your main/mission-critical workflow.
What does "support" mean?
-------------------------
Setuptools is a non-profit community-driven open source project and as such
the word "support" is used in a best-effort manner and with limited scope.
For example, it is not always possible to quickly provide fixes for bugs.
We appreciate the patience of the community and incentivise users
impacted by bugs to contribute to fixes in the form of
:doc:`PR submissions </development/developer-guide>`, to speed-up the process.
When we say "a certain feature is supported" we mean that we will do our best
to ensure this feature keeps working as documented.
Note however that, as in any system, unintended breakages may happen.
We appreciate the community understand and `considerate feedback`_.
.. _considerate feedback: https://opensource.how/etiquette/
What to do after the deprecation period ends?
---------------------------------------------
If you have limited development resources and is not able to
devise a migration plan before Setuptools removes a deprecated feature,
you can still resort to restricting the version of Setuptools to be installed.
This usually includes modifying ``[build-system] requires`` in ``pyproject.toml``
and/or specifying ``pip`` :external+pip:ref:`Constraints Files` via
the ``PIP_CONSTRAINT`` environment variable (or passing |build-constraint-uv|_).
Please avoid however to pre-emptively add version constraints if not necessary,
(you can read more about this in https://iscinumpy.dev/post/bound-version-constraints/).
.. |build-constraint-uv| replace:: ``--build-constraint`` to ``uv``
.. _build-constraint-uv: https://docs.astral.sh/uv/concepts/projects/build/#build-constraints
A note on "Public Names"
------------------------
Python devs may be used to the convention that private members are prefixed
with an ``_`` (underscore) character and that any member not marked by this
public. Due to the history and legacy of Setuptools this is not necessarily
the case [#private]_.
In this project, "public interfaces" are defined as interfaces explicitly
documented for 3rd party consumption.
When accessing a member in the ``setuptools`` package, please make sure it is
documented for external usage. Also note that names imported from different
modules/submodules are considered internal implementation details unless
explicitly listed in ``__all__``. The fact that they are accessible in the
namespace of the ``import``-er module is a mere side effect of the way Python works.
.. [#private]
While names prefixed by ``_`` are always considered private,
not necessary the absence of the prefix signals public members.
import functools
import operator
import packaging.requirements
# from coherent.build.discovery
def extras_from_dep(dep):
try:
markers = packaging.requirements.Requirement(dep).marker._markers
except AttributeError:
markers = ()
return set(
marker[2].value
for marker in markers
if isinstance(marker, tuple) and marker[0].value == 'extra'
)
def extras_from_deps(deps):
"""
>>> extras_from_deps(['requests'])
set()
>>> extras_from_deps(['pytest; extra == "test"'])
{'test'}
>>> sorted(extras_from_deps([
... 'requests',
... 'pytest; extra == "test"',
... 'pytest-cov; extra == "test"',
... 'sphinx; extra=="doc"']))
['doc', 'test']
"""
return functools.reduce(operator.or_, map(extras_from_dep, deps), set())
# 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
"""distutils.ccompiler
Contains Compiler, an abstract base class that defines the interface
for the Distutils compiler abstraction model."""
from __future__ import annotations
import os
import pathlib
import re
import sys
import warnings
from collections.abc import Callable, Iterable, MutableSequence, Sequence
from typing import (
TYPE_CHECKING,
ClassVar,
Literal,
TypeVar,
Union,
overload,
)
from more_itertools import always_iterable
from ..._log import log
from ..._modified import newer_group
from ...dir_util import mkpath
from ...errors import (
DistutilsModuleError,
DistutilsPlatformError,
)
from ...file_util import move_file
from ...spawn import spawn
from ...util import execute, is_mingw, split_quoted
from .errors import (
CompileError,
LinkError,
UnknownFileType,
)
if TYPE_CHECKING:
from typing_extensions import TypeAlias, TypeVarTuple, Unpack
_Ts = TypeVarTuple("_Ts")
_Macro: TypeAlias = Union[tuple[str], tuple[str, Union[str, None]]]
_StrPathT = TypeVar("_StrPathT", bound="str | os.PathLike[str]")
_BytesPathT = TypeVar("_BytesPathT", bound="bytes | os.PathLike[bytes]")
class Compiler:
"""Abstract base class to define the interface that must be implemented
by real compiler classes. Also has some utility methods used by
several compiler classes.
The basic idea behind a compiler abstraction class is that each
instance can be used for all the compile/link steps in building a
single project. Thus, attributes common to all of those compile and
link steps -- include directories, macros to define, libraries to link
against, etc. -- are attributes of the compiler instance. To allow for
variability in how individual files are treated, most of those
attributes may be varied on a per-compilation or per-link basis.
"""
# 'compiler_type' is a class attribute that identifies this class. It
# keeps code that wants to know what kind of compiler it's dealing with
# from having to import all possible compiler classes just to do an
# 'isinstance'. In concrete CCompiler subclasses, 'compiler_type'
# should really, really be one of the keys of the 'compiler_class'
# dictionary (see below -- used by the 'new_compiler()' factory
# function) -- authors of new compiler interface classes are
# responsible for updating 'compiler_class'!
compiler_type: ClassVar[str] = None # type: ignore[assignment]
# XXX things not handled by this compiler abstraction model:
# * client can't provide additional options for a compiler,
# e.g. warning, optimization, debugging flags. Perhaps this
# should be the domain of concrete compiler abstraction classes
# (UnixCCompiler, MSVCCompiler, etc.) -- or perhaps the base
# class should have methods for the common ones.
# * can't completely override the include or library searchg
# path, ie. no "cc -I -Idir1 -Idir2" or "cc -L -Ldir1 -Ldir2".
# I'm not sure how widely supported this is even by Unix
# compilers, much less on other platforms. And I'm even less
# sure how useful it is; maybe for cross-compiling, but
# support for that is a ways off. (And anyways, cross
# compilers probably have a dedicated binary with the
# right paths compiled in. I hope.)
# * can't do really freaky things with the library list/library
# dirs, e.g. "-Ldir1 -lfoo -Ldir2 -lfoo" to link against
# different versions of libfoo.a in different locations. I
# think this is useless without the ability to null out the
# library search path anyways.
executables: ClassVar[dict]
# Subclasses that rely on the standard filename generation methods
# implemented below should override these; see the comment near
# those methods ('object_filenames()' et. al.) for details:
src_extensions: ClassVar[list[str] | None] = None
obj_extension: ClassVar[str | None] = None
static_lib_extension: ClassVar[str | None] = None
shared_lib_extension: ClassVar[str | None] = None
static_lib_format: ClassVar[str | None] = None # format string
shared_lib_format: ClassVar[str | None] = None # prob. same as static_lib_format
exe_extension: ClassVar[str | None] = None
# Default language settings. language_map is used to detect a source
# file or Extension target language, checking source filenames.
# language_order is used to detect the language precedence, when deciding
# what language to use when mixing source types. For example, if some
# extension has two files with ".c" extension, and one with ".cpp", it
# is still linked as c++.
language_map: ClassVar[dict[str, str]] = {
".c": "c",
".cc": "c++",
".cpp": "c++",
".cxx": "c++",
".m": "objc",
}
language_order: ClassVar[list[str]] = ["c++", "objc", "c"]
include_dirs: list[str] = []
"""
include dirs specific to this compiler class
"""
library_dirs: list[str] = []
"""
library dirs specific to this compiler class
"""
def __init__(
self, verbose: bool = False, dry_run: bool = False, force: bool = False
) -> None:
self.dry_run = dry_run
self.force = force
self.verbose = verbose
# 'output_dir': a common output directory for object, library,
# shared object, and shared library files
self.output_dir: str | None = None
# 'macros': a list of macro definitions (or undefinitions). A
# macro definition is a 2-tuple (name, value), where the value is
# either a string or None (no explicit value). A macro
# undefinition is a 1-tuple (name,).
self.macros: list[_Macro] = []
# 'include_dirs': a list of directories to search for include files
self.include_dirs = []
# 'libraries': a list of libraries to include in any link
# (library names, not filenames: eg. "foo" not "libfoo.a")
self.libraries: list[str] = []
# 'library_dirs': a list of directories to search for libraries
self.library_dirs = []
# 'runtime_library_dirs': a list of directories to search for
# shared libraries/objects at runtime
self.runtime_library_dirs: list[str] = []
# 'objects': a list of object files (or similar, such as explicitly
# named library files) to include on any link
self.objects: list[str] = []
for key in self.executables.keys():
self.set_executable(key, self.executables[key])
def set_executables(self, **kwargs: str) -> None:
"""Define the executables (and options for them) that will be run
to perform the various stages of compilation. The exact set of
executables that may be specified here depends on the compiler
class (via the 'executables' class attribute), but most will have:
compiler the C/C++ compiler
linker_so linker used to create shared objects and libraries
linker_exe linker used to create binary executables
archiver static library creator
On platforms with a command-line (Unix, DOS/Windows), each of these
is a string that will be split into executable name and (optional)
list of arguments. (Splitting the string is done similarly to how
Unix shells operate: words are delimited by spaces, but quotes and
backslashes can override this. See
'distutils.util.split_quoted()'.)
"""
# Note that some CCompiler implementation classes will define class
# attributes 'cpp', 'cc', etc. with hard-coded executable names;
# this is appropriate when a compiler class is for exactly one
# compiler/OS combination (eg. MSVCCompiler). Other compiler
# classes (UnixCCompiler, in particular) are driven by information
# discovered at run-time, since there are many different ways to do
# basically the same things with Unix C compilers.
for key in kwargs:
if key not in self.executables:
raise ValueError(
f"unknown executable '{key}' for class {self.__class__.__name__}"
)
self.set_executable(key, kwargs[key])
def set_executable(self, key, value):
if isinstance(value, str):
setattr(self, key, split_quoted(value))
else:
setattr(self, key, value)
def _find_macro(self, name):
i = 0
for defn in self.macros:
if defn[0] == name:
return i
i += 1
return None
def _check_macro_definitions(self, definitions):
"""Ensure that every element of 'definitions' is valid."""
for defn in definitions:
self._check_macro_definition(*defn)
def _check_macro_definition(self, defn):
"""
Raise a TypeError if defn is not valid.
A valid definition is either a (name, value) 2-tuple or a (name,) tuple.
"""
if not isinstance(defn, tuple) or not self._is_valid_macro(*defn):
raise TypeError(
f"invalid macro definition '{defn}': "
"must be tuple (string,), (string, string), or (string, None)"
)
@staticmethod
def _is_valid_macro(name, value=None):
"""
A valid macro is a ``name : str`` and a ``value : str | None``.
>>> Compiler._is_valid_macro('foo', None)
True
"""
return isinstance(name, str) and isinstance(value, (str, type(None)))
# -- Bookkeeping methods -------------------------------------------
def define_macro(self, name: str, value: str | None = None) -> None:
"""Define a preprocessor macro for all compilations driven by this
compiler object. The optional parameter 'value' should be a
string; if it is not supplied, then the macro will be defined
without an explicit value and the exact outcome depends on the
compiler used (XXX true? does ANSI say anything about this?)
"""
# Delete from the list of macro definitions/undefinitions if
# already there (so that this one will take precedence).
i = self._find_macro(name)
if i is not None:
del self.macros[i]
self.macros.append((name, value))
def undefine_macro(self, name: str) -> None:
"""Undefine a preprocessor macro for all compilations driven by
this compiler object. If the same macro is defined by
'define_macro()' and undefined by 'undefine_macro()' the last call
takes precedence (including multiple redefinitions or
undefinitions). If the macro is redefined/undefined on a
per-compilation basis (ie. in the call to 'compile()'), then that
takes precedence.
"""
# Delete from the list of macro definitions/undefinitions if
# already there (so that this one will take precedence).
i = self._find_macro(name)
if i is not None:
del self.macros[i]
undefn = (name,)
self.macros.append(undefn)
def add_include_dir(self, dir: str) -> None:
"""Add 'dir' to the list of directories that will be searched for
header files. The compiler is instructed to search directories in
the order in which they are supplied by successive calls to
'add_include_dir()'.
"""
self.include_dirs.append(dir)
def set_include_dirs(self, dirs: list[str]) -> None:
"""Set the list of directories that will be searched to 'dirs' (a
list of strings). Overrides any preceding calls to
'add_include_dir()'; subsequence calls to 'add_include_dir()' add
to the list passed to 'set_include_dirs()'. This does not affect
any list of standard include directories that the compiler may
search by default.
"""
self.include_dirs = dirs[:]
def add_library(self, libname: str) -> None:
"""Add 'libname' to the list of libraries that will be included in
all links driven by this compiler object. Note that 'libname'
should *not* be the name of a file containing a library, but the
name of the library itself: the actual filename will be inferred by
the linker, the compiler, or the compiler class (depending on the
platform).
The linker will be instructed to link against libraries in the
order they were supplied to 'add_library()' and/or
'set_libraries()'. It is perfectly valid to duplicate library
names; the linker will be instructed to link against libraries as
many times as they are mentioned.
"""
self.libraries.append(libname)
def set_libraries(self, libnames: list[str]) -> None:
"""Set the list of libraries to be included in all links driven by
this compiler object to 'libnames' (a list of strings). This does
not affect any standard system libraries that the linker may
include by default.
"""
self.libraries = libnames[:]
def add_library_dir(self, dir: str) -> None:
"""Add 'dir' to the list of directories that will be searched for
libraries specified to 'add_library()' and 'set_libraries()'. The
linker will be instructed to search for libraries in the order they
are supplied to 'add_library_dir()' and/or 'set_library_dirs()'.
"""
self.library_dirs.append(dir)
def set_library_dirs(self, dirs: list[str]) -> None:
"""Set the list of library search directories to 'dirs' (a list of
strings). This does not affect any standard library search path
that the linker may search by default.
"""
self.library_dirs = dirs[:]
def add_runtime_library_dir(self, dir: str) -> None:
"""Add 'dir' to the list of directories that will be searched for
shared libraries at runtime.
"""
self.runtime_library_dirs.append(dir)
def set_runtime_library_dirs(self, dirs: list[str]) -> None:
"""Set the list of directories to search for shared libraries at
runtime to 'dirs' (a list of strings). This does not affect any
standard search path that the runtime linker may search by
default.
"""
self.runtime_library_dirs = dirs[:]
def add_link_object(self, object: str) -> None:
"""Add 'object' to the list of object files (or analogues, such as
explicitly named library files or the output of "resource
compilers") to be included in every link driven by this compiler
object.
"""
self.objects.append(object)
def set_link_objects(self, objects: list[str]) -> None:
"""Set the list of object files (or analogues) to be included in
every link to 'objects'. This does not affect any standard object
files that the linker may include by default (such as system
libraries).
"""
self.objects = objects[:]
# -- Private utility methods --------------------------------------
# (here for the convenience of subclasses)
# Helper method to prep compiler in subclass compile() methods
def _setup_compile(
self,
outdir: str | None,
macros: list[_Macro] | None,
incdirs: list[str] | tuple[str, ...] | None,
sources,
depends,
extra,
):
"""Process arguments and decide which source files to compile."""
outdir, macros, incdirs = self._fix_compile_args(outdir, macros, incdirs)
if extra is None:
extra = []
# Get the list of expected output (object) files
objects = self.object_filenames(sources, strip_dir=False, output_dir=outdir)
assert len(objects) == len(sources)
pp_opts = gen_preprocess_options(macros, incdirs)
build = {}
for i in range(len(sources)):
src = sources[i]
obj = objects[i]
ext = os.path.splitext(src)[1]
self.mkpath(os.path.dirname(obj))
build[obj] = (src, ext)
return macros, objects, extra, pp_opts, build
def _get_cc_args(self, pp_opts, debug, before):
# works for unixccompiler, cygwinccompiler
cc_args = pp_opts + ['-c']
if debug:
cc_args[:0] = ['-g']
if before:
cc_args[:0] = before
return cc_args
def _fix_compile_args(
self,
output_dir: str | None,
macros: list[_Macro] | None,
include_dirs: list[str] | tuple[str, ...] | None,
) -> tuple[str, list[_Macro], list[str]]:
"""Typecheck and fix-up some of the arguments to the 'compile()'
method, and return fixed-up values. Specifically: if 'output_dir'
is None, replaces it with 'self.output_dir'; ensures that 'macros'
is a list, and augments it with 'self.macros'; ensures that
'include_dirs' is a list, and augments it with 'self.include_dirs'.
Guarantees that the returned values are of the correct type,
i.e. for 'output_dir' either string or None, and for 'macros' and
'include_dirs' either list or None.
"""
if output_dir is None:
output_dir = self.output_dir
elif not isinstance(output_dir, str):
raise TypeError("'output_dir' must be a string or None")
if macros is None:
macros = list(self.macros)
elif isinstance(macros, list):
macros = macros + (self.macros or [])
else:
raise TypeError("'macros' (if supplied) must be a list of tuples")
if include_dirs is None:
include_dirs = list(self.include_dirs)
elif isinstance(include_dirs, (list, tuple)):
include_dirs = list(include_dirs) + (self.include_dirs or [])
else:
raise TypeError("'include_dirs' (if supplied) must be a list of strings")
# add include dirs for class
include_dirs += self.__class__.include_dirs
return output_dir, macros, include_dirs
def _prep_compile(self, sources, output_dir, depends=None):
"""Decide which source files must be recompiled.
Determine the list of object files corresponding to 'sources',
and figure out which ones really need to be recompiled.
Return a list of all object files and a dictionary telling
which source files can be skipped.
"""
# Get the list of expected output (object) files
objects = self.object_filenames(sources, output_dir=output_dir)
assert len(objects) == len(sources)
# Return an empty dict for the "which source files can be skipped"
# return value to preserve API compatibility.
return objects, {}
def _fix_object_args(
self, objects: list[str] | tuple[str, ...], output_dir: str | None
) -> tuple[list[str], str]:
"""Typecheck and fix up some arguments supplied to various methods.
Specifically: ensure that 'objects' is a list; if output_dir is
None, replace with self.output_dir. Return fixed versions of
'objects' and 'output_dir'.
"""
if not isinstance(objects, (list, tuple)):
raise TypeError("'objects' must be a list or tuple of strings")
objects = list(objects)
if output_dir is None:
output_dir = self.output_dir
elif not isinstance(output_dir, str):
raise TypeError("'output_dir' must be a string or None")
return (objects, output_dir)
def _fix_lib_args(
self,
libraries: list[str] | tuple[str, ...] | None,
library_dirs: list[str] | tuple[str, ...] | None,
runtime_library_dirs: list[str] | tuple[str, ...] | None,
) -> tuple[list[str], list[str], list[str]]:
"""Typecheck and fix up some of the arguments supplied to the
'link_*' methods. Specifically: ensure that all arguments are
lists, and augment them with their permanent versions
(eg. 'self.libraries' augments 'libraries'). Return a tuple with
fixed versions of all arguments.
"""
if libraries is None:
libraries = list(self.libraries)
elif isinstance(libraries, (list, tuple)):
libraries = list(libraries) + (self.libraries or [])
else:
raise TypeError("'libraries' (if supplied) must be a list of strings")
if library_dirs is None:
library_dirs = list(self.library_dirs)
elif isinstance(library_dirs, (list, tuple)):
library_dirs = list(library_dirs) + (self.library_dirs or [])
else:
raise TypeError("'library_dirs' (if supplied) must be a list of strings")
# add library dirs for class
library_dirs += self.__class__.library_dirs
if runtime_library_dirs is None:
runtime_library_dirs = list(self.runtime_library_dirs)
elif isinstance(runtime_library_dirs, (list, tuple)):
runtime_library_dirs = list(runtime_library_dirs) + (
self.runtime_library_dirs or []
)
else:
raise TypeError(
"'runtime_library_dirs' (if supplied) must be a list of strings"
)
return (libraries, library_dirs, runtime_library_dirs)
def _need_link(self, objects, output_file):
"""Return true if we need to relink the files listed in 'objects'
to recreate 'output_file'.
"""
if self.force:
return True
else:
if self.dry_run:
newer = newer_group(objects, output_file, missing='newer')
else:
newer = newer_group(objects, output_file)
return newer
def detect_language(self, sources: str | list[str]) -> str | None:
"""Detect the language of a given file, or list of files. Uses
language_map, and language_order to do the job.
"""
if not isinstance(sources, list):
sources = [sources]
lang = None
index = len(self.language_order)
for source in sources:
base, ext = os.path.splitext(source)
extlang = self.language_map.get(ext)
try:
extindex = self.language_order.index(extlang)
if extindex < index:
lang = extlang
index = extindex
except ValueError:
pass
return lang
# -- Worker methods ------------------------------------------------
# (must be implemented by subclasses)
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,
):
"""Preprocess a single C/C++ source file, named in 'source'.
Output will be written to file named 'output_file', or stdout if
'output_file' not supplied. 'macros' is a list of macro
definitions as for 'compile()', which will augment the macros set
with 'define_macro()' and 'undefine_macro()'. 'include_dirs' is a
list of directory names that will be added to the default list.
Raises PreprocessError on failure.
"""
pass
def compile(
self,
sources: Sequence[str | os.PathLike[str]],
output_dir: str | None = None,
macros: list[_Macro] | None = None,
include_dirs: list[str] | tuple[str, ...] | None = None,
debug: bool = False,
extra_preargs: list[str] | None = None,
extra_postargs: list[str] | None = None,
depends: list[str] | tuple[str, ...] | None = None,
) -> list[str]:
"""Compile one or more source files.
'sources' must be a list of filenames, most likely C/C++
files, but in reality anything that can be handled by a
particular compiler and compiler class (eg. MSVCCompiler can
handle resource files in 'sources'). Return a list of object
filenames, one per source filename in 'sources'. Depending on
the implementation, not all source files will necessarily be
compiled, but all corresponding object filenames will be
returned.
If 'output_dir' is given, object files will be put under it, while
retaining their original path component. That is, "foo/bar.c"
normally compiles to "foo/bar.o" (for a Unix implementation); if
'output_dir' is "build", then it would compile to
"build/foo/bar.o".
'macros', if given, must be a list of macro definitions. A macro
definition is either a (name, value) 2-tuple or a (name,) 1-tuple.
The former defines a macro; if the value is None, the macro is
defined without an explicit value. The 1-tuple case undefines a
macro. Later definitions/redefinitions/ undefinitions take
precedence.
'include_dirs', if given, must be a list of strings, the
directories to add to the default include file search path for this
compilation only.
'debug' is a boolean; if true, the compiler will be instructed to
output debug symbols in (or alongside) the object file(s).
'extra_preargs' and 'extra_postargs' are implementation- dependent.
On platforms that have the notion of a command-line (e.g. Unix,
DOS/Windows), they are most likely lists of strings: extra
command-line arguments to prepend/append to the compiler command
line. On other platforms, consult the implementation class
documentation. In any event, they are intended as an escape hatch
for those occasions when the abstract compiler framework doesn't
cut the mustard.
'depends', if given, is a list of filenames that all targets
depend on. If a source file is older than any file in
depends, then the source file will be recompiled. This
supports dependency tracking, but only at a coarse
granularity.
Raises CompileError on failure.
"""
# A concrete compiler class can either override this method
# entirely or implement _compile().
macros, objects, extra_postargs, pp_opts, build = self._setup_compile(
output_dir, macros, include_dirs, sources, depends, extra_postargs
)
cc_args = self._get_cc_args(pp_opts, debug, extra_preargs)
for obj in objects:
try:
src, ext = build[obj]
except KeyError:
continue
self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts)
# Return *all* object filenames, not just the ones we just built.
return objects
def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
"""Compile 'src' to product 'obj'."""
# A concrete compiler class that does not override compile()
# should implement _compile().
pass
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:
"""Link a bunch of stuff together to create a static library file.
The "bunch of stuff" consists of the list of object files supplied
as 'objects', the extra object files supplied to
'add_link_object()' and/or 'set_link_objects()', the libraries
supplied to 'add_library()' and/or 'set_libraries()', and the
libraries supplied as 'libraries' (if any).
'output_libname' should be a library name, not a filename; the
filename will be inferred from the library name. 'output_dir' is
the directory where the library file will be put.
'debug' is a boolean; if true, debugging information will be
included in the library (note that on most platforms, it is the
compile step where this matters: the 'debug' flag is included here
just for consistency).
'target_lang' is the target language for which the given objects
are being compiled. This allows specific linkage time treatment of
certain languages.
Raises LibError on failure.
"""
pass
# values for target_desc parameter in link()
SHARED_OBJECT = "shared_object"
SHARED_LIBRARY = "shared_library"
EXECUTABLE = "executable"
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: list[str] | None = None,
build_temp: str | os.PathLike[str] | None = None,
target_lang: str | None = None,
):
"""Link a bunch of stuff together to create an executable or
shared library file.
The "bunch of stuff" consists of the list of object files supplied
as 'objects'. 'output_filename' should be a filename. If
'output_dir' is supplied, 'output_filename' is relative to it
(i.e. 'output_filename' can provide directory components if
needed).
'libraries' is a list of libraries to link against. These are
library names, not filenames, since they're translated into
filenames in a platform-specific way (eg. "foo" becomes "libfoo.a"
on Unix and "foo.lib" on DOS/Windows). However, they can include a
directory component, which means the linker will look in that
specific directory rather than searching all the normal locations.
'library_dirs', if supplied, should be a list of directories to
search for libraries that were specified as bare library names
(ie. no directory component). These are on top of the system
default and those supplied to 'add_library_dir()' and/or
'set_library_dirs()'. 'runtime_library_dirs' is a list of
directories that will be embedded into the shared library and used
to search for other shared libraries that *it* depends on at
run-time. (This may only be relevant on Unix.)
'export_symbols' is a list of symbols that the shared library will
export. (This appears to be relevant only on Windows.)
'debug' is as for 'compile()' and 'create_static_lib()', with the
slight distinction that it actually matters on most platforms (as
opposed to 'create_static_lib()', which includes a 'debug' flag
mostly for form's sake).
'extra_preargs' and 'extra_postargs' are as for 'compile()' (except
of course that they supply command-line arguments for the
particular linker being used).
'target_lang' is the target language for which the given objects
are being compiled. This allows specific linkage time treatment of
certain languages.
Raises LinkError on failure.
"""
raise NotImplementedError
# Old 'link_*()' methods, rewritten to use the new 'link()' method.
def link_shared_lib(
self,
objects: list[str] | tuple[str, ...],
output_libname: 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: list[str] | None = None,
build_temp: str | os.PathLike[str] | None = None,
target_lang: str | None = None,
):
self.link(
Compiler.SHARED_LIBRARY,
objects,
self.library_filename(output_libname, lib_type='shared'),
output_dir,
libraries,
library_dirs,
runtime_library_dirs,
export_symbols,
debug,
extra_preargs,
extra_postargs,
build_temp,
target_lang,
)
def link_shared_object(
self,
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: list[str] | None = None,
build_temp: str | os.PathLike[str] | None = None,
target_lang: str | None = None,
):
self.link(
Compiler.SHARED_OBJECT,
objects,
output_filename,
output_dir,
libraries,
library_dirs,
runtime_library_dirs,
export_symbols,
debug,
extra_preargs,
extra_postargs,
build_temp,
target_lang,
)
def link_executable(
self,
objects: list[str] | tuple[str, ...],
output_progname: 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,
debug: bool = False,
extra_preargs: list[str] | None = None,
extra_postargs: list[str] | None = None,
target_lang: str | None = None,
):
self.link(
Compiler.EXECUTABLE,
objects,
self.executable_filename(output_progname),
output_dir,
libraries,
library_dirs,
runtime_library_dirs,
None,
debug,
extra_preargs,
extra_postargs,
None,
target_lang,
)
# -- Miscellaneous methods -----------------------------------------
# These are all used by the 'gen_lib_options() function; there is
# no appropriate default implementation so subclasses should
# implement all of these.
def library_dir_option(self, dir: str) -> str:
"""Return the compiler option to add 'dir' to the list of
directories searched for libraries.
"""
raise NotImplementedError
def runtime_library_dir_option(self, dir: str) -> str:
"""Return the compiler option to add 'dir' to the list of
directories searched for runtime libraries.
"""
raise NotImplementedError
def library_option(self, lib: str) -> str:
"""Return the compiler option to add 'lib' to the list of libraries
linked into the shared library or executable.
"""
raise NotImplementedError
def has_function( # noqa: C901
self,
funcname: str,
includes: Iterable[str] | None = None,
include_dirs: list[str] | tuple[str, ...] | None = None,
libraries: list[str] | None = None,
library_dirs: list[str] | tuple[str, ...] | None = None,
) -> bool:
"""Return a boolean indicating whether funcname is provided as
a symbol on the current platform. The optional arguments can
be used to augment the compilation environment.
The libraries argument is a list of flags to be passed to the
linker to make additional symbol definitions available for
linking.
The includes and include_dirs arguments are deprecated.
Usually, supplying include files with function declarations
will cause function detection to fail even in cases where the
symbol is available for linking.
"""
# this can't be included at module scope because it tries to
# import math which might not be available at that point - maybe
# the necessary logic should just be inlined?
import tempfile
if includes is None:
includes = []
else:
warnings.warn("includes is deprecated", DeprecationWarning)
if include_dirs is None:
include_dirs = []
else:
warnings.warn("include_dirs is deprecated", DeprecationWarning)
if libraries is None:
libraries = []
if library_dirs is None:
library_dirs = []
fd, fname = tempfile.mkstemp(".c", funcname, text=True)
with os.fdopen(fd, "w", encoding='utf-8') as f:
for incl in includes:
f.write(f"""#include "{incl}"\n""")
if not includes:
# Use "char func(void);" as the prototype to follow
# what autoconf does. This prototype does not match
# any well-known function the compiler might recognize
# as a builtin, so this ends up as a true link test.
# Without a fake prototype, the test would need to
# know the exact argument types, and the has_function
# interface does not provide that level of information.
f.write(
f"""\
#ifdef __cplusplus
extern "C"
#endif
char {funcname}(void);
"""
)
f.write(
f"""\
int main (int argc, char **argv) {{
{funcname}();
return 0;
}}
"""
)
try:
objects = self.compile([fname], include_dirs=include_dirs)
except CompileError:
return False
finally:
os.remove(fname)
try:
self.link_executable(
objects, "a.out", libraries=libraries, library_dirs=library_dirs
)
except (LinkError, TypeError):
return False
else:
os.remove(
self.executable_filename("a.out", output_dir=self.output_dir or '')
)
finally:
for fn in objects:
os.remove(fn)
return True
def find_library_file(
self, dirs: Iterable[str], lib: str, debug: bool = False
) -> str | None:
"""Search the specified list of directories for a static or shared
library file 'lib' and return the full path to that file. If
'debug' true, look for a debugging version (if that makes sense on
the current platform). Return None if 'lib' wasn't found in any of
the specified directories.
"""
raise NotImplementedError
# -- Filename generation methods -----------------------------------
# The default implementation of the filename generating methods are
# prejudiced towards the Unix/DOS/Windows view of the world:
# * object files are named by replacing the source file extension
# (eg. .c/.cpp -> .o/.obj)
# * library files (shared or static) are named by plugging the
# library name and extension into a format string, eg.
# "lib%s.%s" % (lib_name, ".a") for Unix static libraries
# * executables are named by appending an extension (possibly
# empty) to the program name: eg. progname + ".exe" for
# Windows
#
# To reduce redundant code, these methods expect to find
# several attributes in the current object (presumably defined
# as class attributes):
# * src_extensions -
# list of C/C++ source file extensions, eg. ['.c', '.cpp']
# * obj_extension -
# object file extension, eg. '.o' or '.obj'
# * static_lib_extension -
# extension for static library files, eg. '.a' or '.lib'
# * shared_lib_extension -
# extension for shared library/object files, eg. '.so', '.dll'
# * static_lib_format -
# format string for generating static library filenames,
# eg. 'lib%s.%s' or '%s.%s'
# * shared_lib_format
# format string for generating shared library filenames
# (probably same as static_lib_format, since the extension
# is one of the intended parameters to the format string)
# * exe_extension -
# extension for executable files, eg. '' or '.exe'
def object_filenames(
self,
source_filenames: Iterable[str | os.PathLike[str]],
strip_dir: bool = False,
output_dir: str | os.PathLike[str] | None = '',
) -> list[str]:
if output_dir is None:
output_dir = ''
return list(
self._make_out_path(output_dir, strip_dir, src_name)
for src_name in source_filenames
)
@property
def out_extensions(self):
return dict.fromkeys(self.src_extensions, self.obj_extension)
def _make_out_path(self, output_dir, strip_dir, src_name):
return self._make_out_path_exts(
output_dir, strip_dir, src_name, self.out_extensions
)
@classmethod
def _make_out_path_exts(cls, output_dir, strip_dir, src_name, extensions):
r"""
>>> exts = {'.c': '.o'}
>>> Compiler._make_out_path_exts('.', False, '/foo/bar.c', exts).replace('\\', '/')
'./foo/bar.o'
>>> Compiler._make_out_path_exts('.', True, '/foo/bar.c', exts).replace('\\', '/')
'./bar.o'
"""
src = pathlib.PurePath(src_name)
# Ensure base is relative to honor output_dir (python/cpython#37775).
base = cls._make_relative(src)
try:
new_ext = extensions[src.suffix]
except LookupError:
raise UnknownFileType(f"unknown file type '{src.suffix}' (from '{src}')")
if strip_dir:
base = pathlib.PurePath(base.name)
return os.path.join(output_dir, base.with_suffix(new_ext))
@staticmethod
def _make_relative(base: pathlib.Path):
return base.relative_to(base.anchor)
@overload
def shared_object_filename(
self,
basename: str,
strip_dir: Literal[False] = False,
output_dir: str | os.PathLike[str] = "",
) -> str: ...
@overload
def shared_object_filename(
self,
basename: str | os.PathLike[str],
strip_dir: Literal[True],
output_dir: str | os.PathLike[str] = "",
) -> str: ...
def shared_object_filename(
self,
basename: str | os.PathLike[str],
strip_dir: bool = False,
output_dir: str | os.PathLike[str] = '',
) -> str:
assert output_dir is not None
if strip_dir:
basename = os.path.basename(basename)
return os.path.join(output_dir, basename + self.shared_lib_extension)
@overload
def executable_filename(
self,
basename: str,
strip_dir: Literal[False] = False,
output_dir: str | os.PathLike[str] = "",
) -> str: ...
@overload
def executable_filename(
self,
basename: str | os.PathLike[str],
strip_dir: Literal[True],
output_dir: str | os.PathLike[str] = "",
) -> str: ...
def executable_filename(
self,
basename: str | os.PathLike[str],
strip_dir: bool = False,
output_dir: str | os.PathLike[str] = '',
) -> str:
assert output_dir is not None
if strip_dir:
basename = os.path.basename(basename)
return os.path.join(output_dir, basename + (self.exe_extension or ''))
def library_filename(
self,
libname: str,
lib_type: str = "static",
strip_dir: bool = False,
output_dir: str | os.PathLike[str] = "", # or 'shared'
):
assert output_dir is not None
expected = '"static", "shared", "dylib", "xcode_stub"'
if lib_type not in eval(expected):
raise ValueError(f"'lib_type' must be {expected}")
fmt = getattr(self, lib_type + "_lib_format")
ext = getattr(self, lib_type + "_lib_extension")
dir, base = os.path.split(libname)
filename = fmt % (base, ext)
if strip_dir:
dir = ''
return os.path.join(output_dir, dir, filename)
# -- Utility methods -----------------------------------------------
def announce(self, msg: object, level: int = 1) -> None:
log.debug(msg)
def debug_print(self, msg: object) -> None:
from distutils.debug import DEBUG
if DEBUG:
print(msg)
def warn(self, msg: object) -> None:
sys.stderr.write(f"warning: {msg}\n")
def execute(
self,
func: Callable[[Unpack[_Ts]], object],
args: tuple[Unpack[_Ts]],
msg: object = None,
level: int = 1,
) -> None:
execute(func, args, msg, self.dry_run)
def spawn(
self, cmd: MutableSequence[bytes | str | os.PathLike[str]], **kwargs
) -> None:
spawn(cmd, dry_run=self.dry_run, **kwargs)
@overload
def move_file(
self, src: str | os.PathLike[str], dst: _StrPathT
) -> _StrPathT | str: ...
@overload
def move_file(
self, src: bytes | os.PathLike[bytes], dst: _BytesPathT
) -> _BytesPathT | bytes: ...
def move_file(
self,
src: str | os.PathLike[str] | bytes | os.PathLike[bytes],
dst: str | os.PathLike[str] | bytes | os.PathLike[bytes],
) -> str | os.PathLike[str] | bytes | os.PathLike[bytes]:
return move_file(src, dst, dry_run=self.dry_run)
def mkpath(self, name, mode=0o777):
mkpath(name, mode, dry_run=self.dry_run)
# Map a sys.platform/os.name ('posix', 'nt') to the default compiler
# type for that platform. Keys are interpreted as re match
# patterns. Order is important; platform mappings are preferred over
# OS names.
_default_compilers = (
# Platform string mappings
# on a cygwin built python we can use gcc like an ordinary UNIXish
# compiler
('cygwin.*', 'unix'),
('zos', 'zos'),
# OS name mappings
('posix', 'unix'),
('nt', 'msvc'),
)
def get_default_compiler(osname: str | None = None, platform: str | None = None) -> str:
"""Determine the default compiler to use for the given platform.
osname should be one of the standard Python OS names (i.e. the
ones returned by os.name) and platform the common value
returned by sys.platform for the platform in question.
The default values are os.name and sys.platform in case the
parameters are not given.
"""
if osname is None:
osname = os.name
if platform is None:
platform = sys.platform
# Mingw is a special case where sys.platform is 'win32' but we
# want to use the 'mingw32' compiler, so check it first
if is_mingw():
return 'mingw32'
for pattern, compiler in _default_compilers:
if (
re.match(pattern, platform) is not None
or re.match(pattern, osname) is not None
):
return compiler
# Default to Unix compiler
return 'unix'
# Map compiler types to (module_name, class_name) pairs -- ie. where to
# find the code that implements an interface to this compiler. (The module
# is assumed to be in the 'distutils' package.)
compiler_class = {
'unix': ('unixccompiler', 'UnixCCompiler', "standard UNIX-style compiler"),
'msvc': ('_msvccompiler', 'MSVCCompiler', "Microsoft Visual C++"),
'cygwin': (
'cygwinccompiler',
'CygwinCCompiler',
"Cygwin port of GNU C Compiler for Win32",
),
'mingw32': (
'cygwinccompiler',
'Mingw32CCompiler',
"Mingw32 port of GNU C Compiler for Win32",
),
'bcpp': ('bcppcompiler', 'BCPPCompiler', "Borland C++ Compiler"),
'zos': ('zosccompiler', 'zOSCCompiler', 'IBM XL C/C++ Compilers'),
}
def show_compilers() -> None:
"""Print list of available compilers (used by the "--help-compiler"
options to "build", "build_ext", "build_clib").
"""
# XXX this "knows" that the compiler option it's describing is
# "--compiler", which just happens to be the case for the three
# commands that use it.
from distutils.fancy_getopt import FancyGetopt
compilers = sorted(
("compiler=" + compiler, None, compiler_class[compiler][2])
for compiler in compiler_class.keys()
)
pretty_printer = FancyGetopt(compilers)
pretty_printer.print_help("List of available compilers:")
def new_compiler(
plat: str | None = None,
compiler: str | None = None,
verbose: bool = False,
dry_run: bool = False,
force: bool = False,
) -> Compiler:
"""Generate an instance of some CCompiler subclass for the supplied
platform/compiler combination. 'plat' defaults to 'os.name'
(eg. 'posix', 'nt'), and 'compiler' defaults to the default compiler
for that platform. Currently only 'posix' and 'nt' are supported, and
the default compilers are "traditional Unix interface" (UnixCCompiler
class) and Visual C++ (MSVCCompiler class). Note that it's perfectly
possible to ask for a Unix compiler object under Windows, and a
Microsoft compiler object under Unix -- if you supply a value for
'compiler', 'plat' is ignored.
"""
if plat is None:
plat = os.name
try:
if compiler is None:
compiler = get_default_compiler(plat)
(module_name, class_name, long_description) = compiler_class[compiler]
except KeyError:
msg = f"don't know how to compile C/C++ code on platform '{plat}'"
if compiler is not None:
msg = msg + f" with '{compiler}' compiler"
raise DistutilsPlatformError(msg)
try:
module_name = "distutils." + module_name
__import__(module_name)
module = sys.modules[module_name]
klass = vars(module)[class_name]
except ImportError:
raise DistutilsModuleError(
f"can't compile C/C++ code: unable to load module '{module_name}'"
)
except KeyError:
raise DistutilsModuleError(
f"can't compile C/C++ code: unable to find class '{class_name}' "
f"in module '{module_name}'"
)
# XXX The None is necessary to preserve backwards compatibility
# with classes that expect verbose to be the first positional
# argument.
return klass(None, dry_run, force)
def gen_preprocess_options(
macros: Iterable[_Macro], include_dirs: Iterable[str]
) -> list[str]:
"""Generate C pre-processor options (-D, -U, -I) as used by at least
two types of compilers: the typical Unix compiler and Visual C++.
'macros' is the usual thing, a list of 1- or 2-tuples, where (name,)
means undefine (-U) macro 'name', and (name,value) means define (-D)
macro 'name' to 'value'. 'include_dirs' is just a list of directory
names to be added to the header file search path (-I). Returns a list
of command-line options suitable for either Unix compilers or Visual
C++.
"""
# XXX it would be nice (mainly aesthetic, and so we don't generate
# stupid-looking command lines) to go over 'macros' and eliminate
# redundant definitions/undefinitions (ie. ensure that only the
# latest mention of a particular macro winds up on the command
# line). I don't think it's essential, though, since most (all?)
# Unix C compilers only pay attention to the latest -D or -U
# mention of a macro on their command line. Similar situation for
# 'include_dirs'. I'm punting on both for now. Anyways, weeding out
# redundancies like this should probably be the province of
# CCompiler, since the data structures used are inherited from it
# and therefore common to all CCompiler classes.
pp_opts = []
for macro in macros:
if not (isinstance(macro, tuple) and 1 <= len(macro) <= 2):
raise TypeError(
f"bad macro definition '{macro}': "
"each element of 'macros' list must be a 1- or 2-tuple"
)
if len(macro) == 1: # undefine this macro
pp_opts.append(f"-U{macro[0]}")
elif len(macro) == 2:
if macro[1] is None: # define with no explicit value
pp_opts.append(f"-D{macro[0]}")
else:
# XXX *don't* need to be clever about quoting the
# macro value here, because we're going to avoid the
# shell at all costs when we spawn the command!
pp_opts.append("-D{}={}".format(*macro))
pp_opts.extend(f"-I{dir}" for dir in include_dirs)
return pp_opts
def gen_lib_options(
compiler: Compiler,
library_dirs: Iterable[str],
runtime_library_dirs: Iterable[str],
libraries: Iterable[str],
) -> list[str]:
"""Generate linker options for searching library directories and
linking with specific libraries. 'libraries' and 'library_dirs' are,
respectively, lists of library names (not filenames!) and search
directories. Returns a list of command-line options suitable for use
with some compiler (depending on the two format strings passed in).
"""
lib_opts = [compiler.library_dir_option(dir) for dir in library_dirs]
for dir in runtime_library_dirs:
lib_opts.extend(always_iterable(compiler.runtime_library_dir_option(dir)))
# XXX it's important that we *not* remove redundant library mentions!
# sometimes you really do have to say "-lfoo -lbar -lfoo" in order to
# resolve all symbols. I just hope we never have to say "-lfoo obj.o
# -lbar" to get things to work -- that's certainly a possibility, but a
# pretty nasty way to arrange your C code.
for lib in libraries:
(lib_dir, lib_name) = os.path.split(lib)
if lib_dir:
lib_file = compiler.find_library_file([lib_dir], lib_name)
if lib_file:
lib_opts.append(lib_file)
else:
compiler.warn(
f"no library file corresponding to '{lib}' found (skipping)"
)
else:
lib_opts.append(compiler.library_option(lib))
return lib_opts
"""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.
"""
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."""
"""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
import platform
import sysconfig
import textwrap
import pytest
from .. import base
pytestmark = pytest.mark.usefixtures('suppress_path_mangle')
@pytest.fixture
def c_file(tmp_path):
c_file = tmp_path / 'foo.c'
gen_headers = ('Python.h',)
is_windows = platform.system() == "Windows"
plat_headers = ('windows.h',) * is_windows
all_headers = gen_headers + plat_headers
headers = '\n'.join(f'#include <{header}>\n' for header in all_headers)
payload = (
textwrap.dedent(
"""
#headers
void PyInit_foo(void) {}
"""
)
.lstrip()
.replace('#headers', headers)
)
c_file.write_text(payload, encoding='utf-8')
return c_file
def test_set_include_dirs(c_file):
"""
Extensions should build even if set_include_dirs is invoked.
In particular, compiler-specific paths should not be overridden.
"""
compiler = base.new_compiler()
python = sysconfig.get_paths()['include']
compiler.set_include_dirs([python])
compiler.compile([c_file])
# do it again, setting include dirs after any initialization
compiler.set_include_dirs([python])
compiler.compile([c_file])
def test_has_function_prototype():
# Issue https://github.com/pypa/setuptools/issues/3648
# Test prototype-generating behavior.
compiler = base.new_compiler()
# Every C implementation should have these.
assert compiler.has_function('abort')
assert compiler.has_function('exit')
with pytest.deprecated_call(match='includes is deprecated'):
# abort() is a valid expression with the <stdlib.h> prototype.
assert compiler.has_function('abort', includes=['stdlib.h'])
with pytest.deprecated_call(match='includes is deprecated'):
# But exit() is not valid with the actual prototype in scope.
assert not compiler.has_function('exit', includes=['stdlib.h'])
# And setuptools_does_not_exist is not declared or defined at all.
assert not compiler.has_function('setuptools_does_not_exist')
with pytest.deprecated_call(match='includes is deprecated'):
assert not compiler.has_function(
'setuptools_does_not_exist', includes=['stdio.h']
)
def test_include_dirs_after_multiple_compile_calls(c_file):
"""
Calling compile multiple times should not change the include dirs
(regression test for setuptools issue #3591).
"""
compiler = base.new_compiler()
python = sysconfig.get_paths()['include']
compiler.set_include_dirs([python])
compiler.compile([c_file])
assert compiler.include_dirs == [python]
compiler.compile([c_file])
assert compiler.include_dirs == [python]
"""Tests for distutils.cygwinccompiler."""
import os
import sys
from distutils import sysconfig
from distutils.tests import support
import pytest
from .. import cygwin
@pytest.fixture(autouse=True)
def stuff(request, monkeypatch, distutils_managed_tempdir):
self = request.instance
self.python_h = os.path.join(self.mkdtemp(), 'python.h')
monkeypatch.setattr(sysconfig, 'get_config_h_filename', self._get_config_h_filename)
monkeypatch.setattr(sys, 'version', sys.version)
class TestCygwinCCompiler(support.TempdirManager):
def _get_config_h_filename(self):
return self.python_h
@pytest.mark.skipif('sys.platform != "cygwin"')
@pytest.mark.skipif('not os.path.exists("/usr/lib/libbash.dll.a")')
def test_find_library_file(self):
from distutils.cygwinccompiler import CygwinCCompiler
compiler = CygwinCCompiler()
link_name = "bash"
linkable_file = compiler.find_library_file(["/usr/lib"], link_name)
assert linkable_file is not None
assert os.path.exists(linkable_file)
assert linkable_file == f"/usr/lib/lib{link_name:s}.dll.a"
@pytest.mark.skipif('sys.platform != "cygwin"')
def test_runtime_library_dir_option(self):
from distutils.cygwinccompiler import CygwinCCompiler
compiler = CygwinCCompiler()
assert compiler.runtime_library_dir_option('/foo') == []
def test_check_config_h(self):
# check_config_h looks for "GCC" in sys.version first
# returns CONFIG_H_OK if found
sys.version = (
'2.6.1 (r261:67515, Dec 6 2008, 16:42:21) \n[GCC '
'4.0.1 (Apple Computer, Inc. build 5370)]'
)
assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_OK
# then it tries to see if it can find "__GNUC__" in pyconfig.h
sys.version = 'something without the *CC word'
# if the file doesn't exist it returns CONFIG_H_UNCERTAIN
assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_UNCERTAIN
# if it exists but does not contain __GNUC__, it returns CONFIG_H_NOTOK
self.write_file(self.python_h, 'xxx')
assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_NOTOK
# and CONFIG_H_OK if __GNUC__ is found
self.write_file(self.python_h, 'xxx __GNUC__ xxx')
assert cygwin.check_config_h()[0] == cygwin.CONFIG_H_OK
def test_get_msvcr(self):
assert cygwin.get_msvcr() == []
@pytest.mark.skipif('sys.platform != "cygwin"')
def test_dll_libraries_not_none(self):
from distutils.cygwinccompiler import CygwinCCompiler
compiler = CygwinCCompiler()
assert compiler.dll_libraries is not None
from distutils import sysconfig
from distutils.errors import DistutilsPlatformError
from distutils.util import is_mingw, split_quoted
import pytest
from .. import cygwin, errors
class TestMinGW32Compiler:
@pytest.mark.skipif(not is_mingw(), reason='not on mingw')
def test_compiler_type(self):
compiler = cygwin.MinGW32Compiler()
assert compiler.compiler_type == 'mingw32'
@pytest.mark.skipif(not is_mingw(), reason='not on mingw')
def test_set_executables(self, monkeypatch):
monkeypatch.setenv('CC', 'cc')
monkeypatch.setenv('CXX', 'c++')
compiler = cygwin.MinGW32Compiler()
assert compiler.compiler == split_quoted('cc -O -Wall')
assert compiler.compiler_so == split_quoted('cc -shared -O -Wall')
assert compiler.compiler_cxx == split_quoted('c++ -O -Wall')
assert compiler.linker_exe == split_quoted('cc')
assert compiler.linker_so == split_quoted('cc -shared')
@pytest.mark.skipif(not is_mingw(), reason='not on mingw')
def test_runtime_library_dir_option(self):
compiler = cygwin.MinGW32Compiler()
with pytest.raises(DistutilsPlatformError):
compiler.runtime_library_dir_option('/usr/lib')
@pytest.mark.skipif(not is_mingw(), reason='not on mingw')
def test_cygwincc_error(self, monkeypatch):
monkeypatch.setattr(cygwin, 'is_cygwincc', lambda _: True)
with pytest.raises(errors.Error):
cygwin.MinGW32Compiler()
@pytest.mark.skipif('sys.platform == "cygwin"')
def test_customize_compiler_with_msvc_python(self):
# In case we have an MSVC Python build, but still want to use
# MinGW32Compiler, then customize_compiler() shouldn't fail at least.
# https://github.com/pypa/setuptools/issues/4456
compiler = cygwin.MinGW32Compiler()
sysconfig.customize_compiler(compiler)
import os
import sys
import sysconfig
import threading
import unittest.mock as mock
from distutils.errors import DistutilsPlatformError
from distutils.tests import support
from distutils.util import get_platform
import pytest
from .. import msvc
needs_winreg = pytest.mark.skipif('not hasattr(msvc, "winreg")')
class Testmsvccompiler(support.TempdirManager):
def test_no_compiler(self, monkeypatch):
# makes sure query_vcvarsall raises
# a DistutilsPlatformError if the compiler
# is not found
def _find_vcvarsall(plat_spec):
return None, None
monkeypatch.setattr(msvc, '_find_vcvarsall', _find_vcvarsall)
with pytest.raises(DistutilsPlatformError):
msvc._get_vc_env(
'wont find this version',
)
@pytest.mark.skipif(
not sysconfig.get_platform().startswith("win"),
reason="Only run test for non-mingw Windows platforms",
)
@pytest.mark.parametrize(
"plat_name, expected",
[
("win-arm64", "win-arm64"),
("win-amd64", "win-amd64"),
(None, get_platform()),
],
)
def test_cross_platform_compilation_paths(self, monkeypatch, plat_name, expected):
"""
Ensure a specified target platform is passed to _get_vcvars_spec.
"""
compiler = msvc.Compiler()
def _get_vcvars_spec(host_platform, platform):
assert platform == expected
monkeypatch.setattr(msvc, '_get_vcvars_spec', _get_vcvars_spec)
compiler.initialize(plat_name)
@needs_winreg
def test_get_vc_env_unicode(self):
test_var = 'ṰḖṤṪ┅ṼẨṜ'
test_value = '₃⁴₅'
# Ensure we don't early exit from _get_vc_env
old_distutils_use_sdk = os.environ.pop('DISTUTILS_USE_SDK', None)
os.environ[test_var] = test_value
try:
env = msvc._get_vc_env('x86')
assert test_var.lower() in env
assert test_value == env[test_var.lower()]
finally:
os.environ.pop(test_var)
if old_distutils_use_sdk:
os.environ['DISTUTILS_USE_SDK'] = old_distutils_use_sdk
@needs_winreg
@pytest.mark.parametrize('ver', (2015, 2017))
def test_get_vc(self, ver):
# This function cannot be mocked, so pass if VC is found
# and skip otherwise.
lookup = getattr(msvc, f'_find_vc{ver}')
expected_version = {2015: 14, 2017: 15}[ver]
version, path = lookup()
if not version:
pytest.skip(f"VS {ver} is not installed")
assert version >= expected_version
assert os.path.isdir(path)
class CheckThread(threading.Thread):
exc_info = None
def run(self):
try:
super().run()
except Exception:
self.exc_info = sys.exc_info()
def __bool__(self):
return not self.exc_info
class TestSpawn:
def test_concurrent_safe(self):
"""
Concurrent calls to spawn should have consistent results.
"""
compiler = msvc.Compiler()
compiler._paths = "expected"
inner_cmd = 'import os; assert os.environ["PATH"] == "expected"'
command = [sys.executable, '-c', inner_cmd]
threads = [
CheckThread(target=compiler.spawn, args=[command]) for n in range(100)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert all(threads)
def test_concurrent_safe_fallback(self):
"""
If CCompiler.spawn has been monkey-patched without support
for an env, it should still execute.
"""
from distutils import ccompiler
compiler = msvc.Compiler()
compiler._paths = "expected"
def CCompiler_spawn(self, cmd):
"A spawn without an env argument."
assert os.environ["PATH"] == "expected"
with mock.patch.object(ccompiler.CCompiler, 'spawn', CCompiler_spawn):
compiler.spawn(["n/a"])
assert os.environ.get("PATH") != "expected"
"""Tests for distutils.unixccompiler."""
import os
import sys
import unittest.mock as mock
from distutils import sysconfig
from distutils.compat import consolidate_linker_args
from distutils.errors import DistutilsPlatformError
from distutils.tests import support
from distutils.tests.compat.py39 import EnvironmentVarGuard
from distutils.util import _clear_cached_macosx_ver
import pytest
from .. import unix
@pytest.fixture(autouse=True)
def save_values(monkeypatch):
monkeypatch.setattr(sys, 'platform', sys.platform)
monkeypatch.setattr(sysconfig, 'get_config_var', sysconfig.get_config_var)
monkeypatch.setattr(sysconfig, 'get_config_vars', sysconfig.get_config_vars)
@pytest.fixture(autouse=True)
def compiler_wrapper(request):
class CompilerWrapper(unix.Compiler):
def rpath_foo(self):
return self.runtime_library_dir_option('/foo')
request.instance.cc = CompilerWrapper()
class TestUnixCCompiler(support.TempdirManager):
@pytest.mark.skipif('platform.system == "Windows"')
def test_runtime_libdir_option(self): # noqa: C901
# Issue #5900; GitHub Issue #37
#
# Ensure RUNPATH is added to extension modules with RPATH if
# GNU ld is used
# darwin
sys.platform = 'darwin'
darwin_ver_var = 'MACOSX_DEPLOYMENT_TARGET'
darwin_rpath_flag = '-Wl,-rpath,/foo'
darwin_lib_flag = '-L/foo'
# (macOS version from syscfg, macOS version from env var) -> flag
# Version value of None generates two tests: as None and as empty string
# Expected flag value of None means an mismatch exception is expected
darwin_test_cases = [
((None, None), darwin_lib_flag),
((None, '11'), darwin_rpath_flag),
(('10', None), darwin_lib_flag),
(('10.3', None), darwin_lib_flag),
(('10.3.1', None), darwin_lib_flag),
(('10.5', None), darwin_rpath_flag),
(('10.5.1', None), darwin_rpath_flag),
(('10.3', '10.3'), darwin_lib_flag),
(('10.3', '10.5'), darwin_rpath_flag),
(('10.5', '10.3'), darwin_lib_flag),
(('10.5', '11'), darwin_rpath_flag),
(('10.4', '10'), None),
]
def make_darwin_gcv(syscfg_macosx_ver):
def gcv(var):
if var == darwin_ver_var:
return syscfg_macosx_ver
return "xxx"
return gcv
def do_darwin_test(syscfg_macosx_ver, env_macosx_ver, expected_flag):
env = os.environ
msg = f"macOS version = (sysconfig={syscfg_macosx_ver!r}, env={env_macosx_ver!r})"
# Save
old_gcv = sysconfig.get_config_var
old_env_macosx_ver = env.get(darwin_ver_var)
# Setup environment
_clear_cached_macosx_ver()
sysconfig.get_config_var = make_darwin_gcv(syscfg_macosx_ver)
if env_macosx_ver is not None:
env[darwin_ver_var] = env_macosx_ver
elif darwin_ver_var in env:
env.pop(darwin_ver_var)
# Run the test
if expected_flag is not None:
assert self.cc.rpath_foo() == expected_flag, msg
else:
with pytest.raises(
DistutilsPlatformError, match=darwin_ver_var + r' mismatch'
):
self.cc.rpath_foo()
# Restore
if old_env_macosx_ver is not None:
env[darwin_ver_var] = old_env_macosx_ver
elif darwin_ver_var in env:
env.pop(darwin_ver_var)
sysconfig.get_config_var = old_gcv
_clear_cached_macosx_ver()
for macosx_vers, expected_flag in darwin_test_cases:
syscfg_macosx_ver, env_macosx_ver = macosx_vers
do_darwin_test(syscfg_macosx_ver, env_macosx_ver, expected_flag)
# Bonus test cases with None interpreted as empty string
if syscfg_macosx_ver is None:
do_darwin_test("", env_macosx_ver, expected_flag)
if env_macosx_ver is None:
do_darwin_test(syscfg_macosx_ver, "", expected_flag)
if syscfg_macosx_ver is None and env_macosx_ver is None:
do_darwin_test("", "", expected_flag)
old_gcv = sysconfig.get_config_var
# hp-ux
sys.platform = 'hp-ux'
def gcv(v):
return 'xxx'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == ['+s', '-L/foo']
def gcv(v):
return 'gcc'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == ['-Wl,+s', '-L/foo']
def gcv(v):
return 'g++'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == ['-Wl,+s', '-L/foo']
sysconfig.get_config_var = old_gcv
# GCC GNULD
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'gcc'
elif v == 'GNULD':
return 'yes'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == consolidate_linker_args([
'-Wl,--enable-new-dtags',
'-Wl,-rpath,/foo',
])
def gcv(v):
if v == 'CC':
return 'gcc -pthread -B /bar'
elif v == 'GNULD':
return 'yes'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == consolidate_linker_args([
'-Wl,--enable-new-dtags',
'-Wl,-rpath,/foo',
])
# GCC non-GNULD
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'gcc'
elif v == 'GNULD':
return 'no'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == '-Wl,-R/foo'
# GCC GNULD with fully qualified configuration prefix
# see #7617
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'x86_64-pc-linux-gnu-gcc-4.4.2'
elif v == 'GNULD':
return 'yes'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == consolidate_linker_args([
'-Wl,--enable-new-dtags',
'-Wl,-rpath,/foo',
])
# non-GCC GNULD
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'cc'
elif v == 'GNULD':
return 'yes'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == consolidate_linker_args([
'-Wl,--enable-new-dtags',
'-Wl,-rpath,/foo',
])
# non-GCC non-GNULD
sys.platform = 'bar'
def gcv(v):
if v == 'CC':
return 'cc'
elif v == 'GNULD':
return 'no'
sysconfig.get_config_var = gcv
assert self.cc.rpath_foo() == '-Wl,-R/foo'
@pytest.mark.skipif('platform.system == "Windows"')
def test_cc_overrides_ldshared(self):
# Issue #18080:
# ensure that setting CC env variable also changes default linker
def gcv(v):
if v == 'LDSHARED':
return 'gcc-4.2 -bundle -undefined dynamic_lookup '
return 'gcc-4.2'
def gcvs(*args, _orig=sysconfig.get_config_vars):
if args:
return list(map(sysconfig.get_config_var, args))
return _orig()
sysconfig.get_config_var = gcv
sysconfig.get_config_vars = gcvs
with EnvironmentVarGuard() as env:
env['CC'] = 'my_cc'
del env['LDSHARED']
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so[0] == 'my_cc'
@pytest.mark.skipif('platform.system == "Windows"')
def test_cxx_commands_used_are_correct(self):
def gcv(v):
if v == 'LDSHARED':
return 'ccache gcc-4.2 -bundle -undefined dynamic_lookup'
elif v == 'LDCXXSHARED':
return 'ccache g++-4.2 -bundle -undefined dynamic_lookup'
elif v == 'CXX':
return 'ccache g++-4.2'
elif v == 'CC':
return 'ccache gcc-4.2'
return ''
def gcvs(*args, _orig=sysconfig.get_config_vars):
if args:
return list(map(sysconfig.get_config_var, args))
return _orig() # pragma: no cover
sysconfig.get_config_var = gcv
sysconfig.get_config_vars = gcvs
with (
mock.patch.object(self.cc, 'spawn', return_value=None) as mock_spawn,
mock.patch.object(self.cc, '_need_link', return_value=True),
mock.patch.object(self.cc, 'mkpath', return_value=None),
EnvironmentVarGuard() as env,
):
# override environment overrides in case they're specified by CI
del env['CXX']
del env['LDCXXSHARED']
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so_cxx[0:2] == ['ccache', 'g++-4.2']
assert self.cc.linker_exe_cxx[0:2] == ['ccache', 'g++-4.2']
self.cc.link(None, [], 'a.out', target_lang='c++')
call_args = mock_spawn.call_args[0][0]
expected = ['ccache', 'g++-4.2', '-bundle', '-undefined', 'dynamic_lookup']
assert call_args[:5] == expected
self.cc.link_executable([], 'a.out', target_lang='c++')
call_args = mock_spawn.call_args[0][0]
expected = ['ccache', 'g++-4.2', '-o', self.cc.executable_filename('a.out')]
assert call_args[:4] == expected
env['LDCXXSHARED'] = 'wrapper g++-4.2 -bundle -undefined dynamic_lookup'
env['CXX'] = 'wrapper g++-4.2'
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so_cxx[0:2] == ['wrapper', 'g++-4.2']
assert self.cc.linker_exe_cxx[0:2] == ['wrapper', 'g++-4.2']
self.cc.link(None, [], 'a.out', target_lang='c++')
call_args = mock_spawn.call_args[0][0]
expected = ['wrapper', 'g++-4.2', '-bundle', '-undefined', 'dynamic_lookup']
assert call_args[:5] == expected
self.cc.link_executable([], 'a.out', target_lang='c++')
call_args = mock_spawn.call_args[0][0]
expected = [
'wrapper',
'g++-4.2',
'-o',
self.cc.executable_filename('a.out'),
]
assert call_args[:4] == expected
@pytest.mark.skipif('platform.system == "Windows"')
@pytest.mark.usefixtures('disable_macos_customization')
def test_cc_overrides_ldshared_for_cxx_correctly(self):
"""
Ensure that setting CC env variable also changes default linker
correctly when building C++ extensions.
pypa/distutils#126
"""
def gcv(v):
if v == 'LDSHARED':
return 'gcc-4.2 -bundle -undefined dynamic_lookup '
elif v == 'LDCXXSHARED':
return 'g++-4.2 -bundle -undefined dynamic_lookup '
elif v == 'CXX':
return 'g++-4.2'
elif v == 'CC':
return 'gcc-4.2'
return ''
def gcvs(*args, _orig=sysconfig.get_config_vars):
if args:
return list(map(sysconfig.get_config_var, args))
return _orig()
sysconfig.get_config_var = gcv
sysconfig.get_config_vars = gcvs
with (
mock.patch.object(self.cc, 'spawn', return_value=None) as mock_spawn,
mock.patch.object(self.cc, '_need_link', return_value=True),
mock.patch.object(self.cc, 'mkpath', return_value=None),
EnvironmentVarGuard() as env,
):
env['CC'] = 'ccache my_cc'
env['CXX'] = 'my_cxx'
del env['LDSHARED']
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so[0:2] == ['ccache', 'my_cc']
self.cc.link(None, [], 'a.out', target_lang='c++')
call_args = mock_spawn.call_args[0][0]
expected = ['my_cxx', '-bundle', '-undefined', 'dynamic_lookup']
assert call_args[:4] == expected
@pytest.mark.skipif('platform.system == "Windows"')
def test_explicit_ldshared(self):
# Issue #18080:
# ensure that setting CC env variable does not change
# explicit LDSHARED setting for linker
def gcv(v):
if v == 'LDSHARED':
return 'gcc-4.2 -bundle -undefined dynamic_lookup '
return 'gcc-4.2'
def gcvs(*args, _orig=sysconfig.get_config_vars):
if args:
return list(map(sysconfig.get_config_var, args))
return _orig()
sysconfig.get_config_var = gcv
sysconfig.get_config_vars = gcvs
with EnvironmentVarGuard() as env:
env['CC'] = 'my_cc'
env['LDSHARED'] = 'my_ld -bundle -dynamic'
sysconfig.customize_compiler(self.cc)
assert self.cc.linker_so[0] == 'my_ld'
def test_has_function(self):
# Issue https://github.com/pypa/distutils/issues/64:
# ensure that setting output_dir does not raise
# FileNotFoundError: [Errno 2] No such file or directory: 'a.out'
self.cc.output_dir = 'scratch'
os.chdir(self.mkdtemp())
self.cc.has_function('abort')
def test_find_library_file(self, monkeypatch):
compiler = unix.Compiler()
compiler._library_root = lambda dir: dir
monkeypatch.setattr(os.path, 'exists', lambda d: 'existing' in d)
libname = 'libabc.dylib' if sys.platform != 'cygwin' else 'cygabc.dll'
dirs = ('/foo/bar/missing', '/foo/bar/existing')
assert (
compiler.find_library_file(dirs, 'abc').replace('\\', '/')
== f'/foo/bar/existing/{libname}'
)
assert (
compiler.find_library_file(reversed(dirs), 'abc').replace('\\', '/')
== f'/foo/bar/existing/{libname}'
)
monkeypatch.setattr(
os.path,
'exists',
lambda d: 'existing' in d and '.a' in d and '.dll.a' not in d,
)
assert (
compiler.find_library_file(dirs, 'abc').replace('\\', '/')
== '/foo/bar/existing/libabc.a'
)
assert (
compiler.find_library_file(reversed(dirs), 'abc').replace('\\', '/')
== '/foo/bar/existing/libabc.a'
)
"""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)
"""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,
)
import sys
if sys.version_info >= (3, 10):
from test.support.import_helper import (
CleanImport as CleanImport,
)
from test.support.import_helper import (
DirsOnSysPath as DirsOnSysPath,
)
from test.support.os_helper import (
EnvironmentVarGuard as EnvironmentVarGuard,
)
from test.support.os_helper import (
rmtree as rmtree,
)
from test.support.os_helper import (
skip_unless_symlink as skip_unless_symlink,
)
from test.support.os_helper import (
unlink as unlink,
)
else:
from test.support import (
CleanImport as CleanImport,
)
from test.support import (
DirsOnSysPath as DirsOnSysPath,
)
from test.support import (
EnvironmentVarGuard as EnvironmentVarGuard,
)
from test.support import (
rmtree as rmtree,
)
from test.support import (
skip_unless_symlink as skip_unless_symlink,
)
from test.support import (
unlink as unlink,
)
from __future__ import annotations
import os
import re
import shlex
import shutil
import struct
import subprocess
import sys
import textwrap
from collections.abc import Iterable
from typing import TYPE_CHECKING, TypedDict
from ._importlib import metadata, resources
if TYPE_CHECKING:
from typing_extensions import Self
from .warnings import SetuptoolsWarning
from distutils.command.build_scripts import first_line_re
from distutils.util import get_platform
class _SplitArgs(TypedDict, total=False):
comments: bool
posix: bool
class CommandSpec(list):
"""
A command spec for a #! header, specified as a list of arguments akin to
those passed to Popen.
"""
options: list[str] = []
split_args = _SplitArgs()
@classmethod
def best(cls):
"""
Choose the best CommandSpec class based on environmental conditions.
"""
return cls
@classmethod
def _sys_executable(cls):
_default = os.path.normpath(sys.executable)
return os.environ.get('__PYVENV_LAUNCHER__', _default)
@classmethod
def from_param(cls, param: Self | str | Iterable[str] | None) -> Self:
"""
Construct a CommandSpec from a parameter to build_scripts, which may
be None.
"""
if isinstance(param, cls):
return param
if isinstance(param, str):
return cls.from_string(param)
if isinstance(param, Iterable):
return cls(param)
if param is None:
return cls.from_environment()
raise TypeError(f"Argument has an unsupported type {type(param)}")
@classmethod
def from_environment(cls):
return cls([cls._sys_executable()])
@classmethod
def from_string(cls, string: str) -> Self:
"""
Construct a command spec from a simple string representing a command
line parseable by shlex.split.
"""
items = shlex.split(string, **cls.split_args)
return cls(items)
def install_options(self, script_text: str):
self.options = shlex.split(self._extract_options(script_text))
cmdline = subprocess.list2cmdline(self)
if not isascii(cmdline):
self.options[:0] = ['-x']
@staticmethod
def _extract_options(orig_script):
"""
Extract any options from the first line of the script.
"""
first = (orig_script + '\n').splitlines()[0]
match = _first_line_re().match(first)
options = match.group(1) or '' if match else ''
return options.strip()
def as_header(self):
return self._render(self + list(self.options))
@staticmethod
def _strip_quotes(item):
_QUOTES = '"\''
for q in _QUOTES:
if item.startswith(q) and item.endswith(q):
return item[1:-1]
return item
@staticmethod
def _render(items):
cmdline = subprocess.list2cmdline(
CommandSpec._strip_quotes(item.strip()) for item in items
)
return '#!' + cmdline + '\n'
class WindowsCommandSpec(CommandSpec):
split_args = _SplitArgs(posix=False)
class ScriptWriter:
"""
Encapsulates behavior around writing entry point scripts for console and
gui apps.
"""
template = textwrap.dedent(
r"""
# EASY-INSTALL-ENTRY-SCRIPT: %(spec)r,%(group)r,%(name)r
import re
import sys
# for compatibility with easy_install; see #2198
__requires__ = %(spec)r
try:
from importlib.metadata import distribution
except ImportError:
try:
from importlib_metadata import distribution
except ImportError:
from pkg_resources import load_entry_point
def importlib_load_entry_point(spec, group, name):
dist_name, _, _ = spec.partition('==')
matches = (
entry_point
for entry_point in distribution(dist_name).entry_points
if entry_point.group == group and entry_point.name == name
)
return next(matches).load()
globals().setdefault('load_entry_point', importlib_load_entry_point)
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
sys.exit(load_entry_point(%(spec)r, %(group)r, %(name)r)())
"""
).lstrip()
command_spec_class = CommandSpec
@classmethod
def get_args(cls, dist, header=None):
"""
Yield write_script() argument tuples for a distribution's
console_scripts and gui_scripts entry points.
"""
# If distribution is not an importlib.metadata.Distribution, assume
# it's a pkg_resources.Distribution and transform it.
if not hasattr(dist, 'entry_points'):
SetuptoolsWarning.emit("Unsupported distribution encountered.")
dist = metadata.Distribution.at(dist.egg_info)
if header is None:
header = cls.get_header()
spec = f'{dist.name}=={dist.version}'
for type_ in 'console', 'gui':
group = f'{type_}_scripts'
for ep in dist.entry_points.select(group=group):
name = ep.name
cls._ensure_safe_name(ep.name)
script_text = cls.template % locals()
args = cls._get_script_args(type_, ep.name, header, script_text)
yield from args
@staticmethod
def _ensure_safe_name(name):
"""
Prevent paths in *_scripts entry point names.
"""
has_path_sep = re.search(r'[\\/]', name)
if has_path_sep:
raise ValueError("Path separators not allowed in script names")
@classmethod
def best(cls):
"""
Select the best ScriptWriter for this environment.
"""
if sys.platform == 'win32' or (os.name == 'java' and os._name == 'nt'):
return WindowsScriptWriter.best()
else:
return cls
@classmethod
def _get_script_args(cls, type_, name, header, script_text):
# Simply write the stub with no extension.
yield (name, header + script_text)
@classmethod
def get_header(
cls,
script_text: str = "",
executable: str | CommandSpec | Iterable[str] | None = None,
) -> str:
"""Create a #! line, getting options (if any) from script_text"""
cmd = cls.command_spec_class.best().from_param(executable)
cmd.install_options(script_text)
return cmd.as_header()
class WindowsScriptWriter(ScriptWriter):
command_spec_class = WindowsCommandSpec
@classmethod
def best(cls):
"""
Select the best ScriptWriter suitable for Windows
"""
writer_lookup = dict(
executable=WindowsExecutableLauncherWriter,
natural=cls,
)
# for compatibility, use the executable launcher by default
launcher = os.environ.get('SETUPTOOLS_LAUNCHER', 'executable')
return writer_lookup[launcher]
@classmethod
def _get_script_args(cls, type_, name, header, script_text):
"For Windows, add a .py extension"
ext = dict(console='.pya', gui='.pyw')[type_]
if ext not in os.environ['PATHEXT'].lower().split(';'):
msg = (
"{ext} not listed in PATHEXT; scripts will not be "
"recognized as executables."
).format(**locals())
SetuptoolsWarning.emit(msg)
old = ['.pya', '.py', '-script.py', '.pyc', '.pyo', '.pyw', '.exe']
old.remove(ext)
header = cls._adjust_header(type_, header)
blockers = [name + x for x in old]
yield name + ext, header + script_text, 't', blockers
@classmethod
def _adjust_header(cls, type_, orig_header):
"""
Make sure 'pythonw' is used for gui and 'python' is used for
console (regardless of what sys.executable is).
"""
pattern = 'pythonw.exe'
repl = 'python.exe'
if type_ == 'gui':
pattern, repl = repl, pattern
pattern_ob = re.compile(re.escape(pattern), re.IGNORECASE)
new_header = pattern_ob.sub(string=orig_header, repl=repl)
return new_header if cls._use_header(new_header) else orig_header
@staticmethod
def _use_header(new_header):
"""
Should _adjust_header use the replaced header?
On non-windows systems, always use. On
Windows systems, only use the replaced header if it resolves
to an executable on the system.
"""
clean_header = new_header[2:-1].strip('"')
return sys.platform != 'win32' or shutil.which(clean_header)
class WindowsExecutableLauncherWriter(WindowsScriptWriter):
@classmethod
def _get_script_args(cls, type_, name, header, script_text):
"""
For Windows, add a .py extension and an .exe launcher
"""
if type_ == 'gui':
launcher_type = 'gui'
ext = '-script.pyw'
old = ['.pyw']
else:
launcher_type = 'cli'
ext = '-script.py'
old = ['.py', '.pyc', '.pyo']
hdr = cls._adjust_header(type_, header)
blockers = [name + x for x in old]
yield (name + ext, hdr + script_text, 't', blockers)
yield (
name + '.exe',
get_win_launcher(launcher_type),
'b', # write in binary mode
)
if not is_64bit():
# install a manifest for the launcher to prevent Windows
# from detecting it as an installer (which it will for
# launchers like easy_install.exe). Consider only
# adding a manifest for launchers detected as installers.
# See Distribute #143 for details.
m_name = name + '.exe.manifest'
yield (m_name, load_launcher_manifest(name), 't')
def get_win_launcher(type):
"""
Load the Windows launcher (executable) suitable for launching a script.
`type` should be either 'cli' or 'gui'
Returns the executable as a byte string.
"""
launcher_fn = f'{type}.exe'
if is_64bit():
if get_platform() == "win-arm64":
launcher_fn = launcher_fn.replace(".", "-arm64.")
else:
launcher_fn = launcher_fn.replace(".", "-64.")
else:
launcher_fn = launcher_fn.replace(".", "-32.")
return resources.files('setuptools').joinpath(launcher_fn).read_bytes()
def load_launcher_manifest(name):
res = resources.files(__name__).joinpath('launcher manifest.xml')
return res.read_text(encoding='utf-8') % vars()
def _first_line_re():
"""
Return a regular expression based on first_line_re suitable for matching
strings.
"""
if isinstance(first_line_re.pattern, str):
return first_line_re
# first_line_re in Python >=3.1.4 and >=3.2.1 is a bytes pattern.
return re.compile(first_line_re.pattern.decode())
def is_64bit():
return struct.calcsize("P") == 8
def isascii(s):
try:
s.encode('ascii')
except UnicodeError:
return False
return True
"""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)
def current_umask():
tmp = os.umask(0o022)
os.umask(tmp)
return tmp
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()

Sorry, the diff of this file is not supported yet

This software is made available under the terms of *either* of the licenses
found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
under the terms of *both* these licenses.

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Metadata-Version: 2.3
Name: packaging
Version: 24.2
Summary: Core utilities for Python packages
Author-email: Donald Stufft <donald@stufft.io>
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Typing :: Typed
Project-URL: Documentation, https://packaging.pypa.io/
Project-URL: Source, https://github.com/pypa/packaging
packaging
=========
.. start-intro
Reusable core utilities for various Python Packaging
`interoperability specifications <https://packaging.python.org/specifications/>`_.
This library provides utilities that implement the interoperability
specifications which have clearly one correct behaviour (eg: :pep:`440`)
or benefit greatly from having a single shared implementation (eg: :pep:`425`).
.. end-intro
The ``packaging`` project includes the following: version handling, specifiers,
markers, requirements, tags, utilities.
Documentation
-------------
The `documentation`_ provides information and the API for the following:
- Version Handling
- Specifiers
- Markers
- Requirements
- Tags
- Utilities
Installation
------------
Use ``pip`` to install these utilities::
pip install packaging
The ``packaging`` library uses calendar-based versioning (``YY.N``).
Discussion
----------
If you run into bugs, you can file them in our `issue tracker`_.
You can also join ``#pypa`` on Freenode to ask questions or get involved.
.. _`documentation`: https://packaging.pypa.io/
.. _`issue tracker`: https://github.com/pypa/packaging/issues
Code of Conduct
---------------
Everyone interacting in the packaging project's codebases, issue trackers, chat
rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
Contributing
------------
The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as
well as how to report a potential security issue. The documentation for this
project also covers information about `project development`_ and `security`_.
.. _`project development`: https://packaging.pypa.io/en/latest/development/
.. _`security`: https://packaging.pypa.io/en/latest/security/
Project History
---------------
Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for
recent changes and project history.
.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/
packaging-24.2.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
packaging-24.2.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
packaging-24.2.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
packaging-24.2.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
packaging-24.2.dist-info/METADATA,sha256=ohH86s6k5mIfQxY2TS0LcSfADeOFa4BiCC-bxZV-pNs,3204
packaging-24.2.dist-info/RECORD,,
packaging-24.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
packaging-24.2.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
packaging/__init__.py,sha256=dk4Ta_vmdVJxYHDcfyhvQNw8V3PgSBomKNXqg-D2JDY,494
packaging/_elffile.py,sha256=cflAQAkE25tzhYmq_aCi72QfbT_tn891tPzfpbeHOwE,3306
packaging/_manylinux.py,sha256=vl5OCoz4kx80H5rwXKeXWjl9WNISGmr4ZgTpTP9lU9c,9612
packaging/_musllinux.py,sha256=p9ZqNYiOItGee8KcZFeHF_YcdhVwGHdK6r-8lgixvGQ,2694
packaging/_parser.py,sha256=s_TvTvDNK0NrM2QB3VKThdWFM4Nc0P6JnkObkl3MjpM,10236
packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431
packaging/_tokenizer.py,sha256=J6v5H7Jzvb-g81xp_2QACKwO7LxHQA6ikryMU7zXwN8,5273
packaging/licenses/__init__.py,sha256=1x5M1nEYjcgwEbLt0dXwz2ukjr18DiCzC0sraQqJ-Ww,5715
packaging/licenses/_spdx.py,sha256=oAm1ztPFwlsmCKe7lAAsv_OIOfS1cWDu9bNBkeu-2ns,48398
packaging/markers.py,sha256=c89TNzB7ZdGYhkovm6PYmqGyHxXlYVaLW591PHUNKD8,10561
packaging/metadata.py,sha256=YJibM7GYe4re8-0a3OlXmGS-XDgTEoO4tlBt2q25Bng,34762
packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
packaging/requirements.py,sha256=gYyRSAdbrIyKDY66ugIDUQjRMvxkH2ALioTmX3tnL6o,2947
packaging/specifiers.py,sha256=GG1wPNMcL0fMJO68vF53wKMdwnfehDcaI-r9NpTfilA,40074
packaging/tags.py,sha256=CFqrJzAzc2XNGexerH__T-Y5Iwq7WbsYXsiLERLWxY0,21014
packaging/utils.py,sha256=0F3Hh9OFuRgrhTgGZUl5K22Fv1YP2tZl1z_2gO6kJiA,5050
packaging/version.py,sha256=olfyuk_DPbflNkJ4wBWetXQ17c74x3DB501degUv7DY,16676

Sorry, the diff of this file is not supported yet

Wheel-Version: 1.0
Generator: flit 3.10.1
Root-Is-Purelib: true
Tag: py3-none-any
#######################################################################################
#
# Adapted from:
# https://github.com/pypa/hatch/blob/5352e44/backend/src/hatchling/licenses/parse.py
#
# MIT License
#
# Copyright (c) 2017-present Ofek Lev <oss@ofek.dev>
#
# 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.
#
#
# With additional allowance of arbitrary `LicenseRef-` identifiers, not just
# `LicenseRef-Public-Domain` and `LicenseRef-Proprietary`.
#
#######################################################################################
from __future__ import annotations
import re
from typing import NewType, cast
from packaging.licenses._spdx import EXCEPTIONS, LICENSES
__all__ = [
"NormalizedLicenseExpression",
"InvalidLicenseExpression",
"canonicalize_license_expression",
]
license_ref_allowed = re.compile("^[A-Za-z0-9.-]*$")
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
class InvalidLicenseExpression(ValueError):
"""Raised when a license-expression string is invalid
>>> canonicalize_license_expression("invalid")
Traceback (most recent call last):
...
packaging.licenses.InvalidLicenseExpression: Invalid license expression: 'invalid'
"""
def canonicalize_license_expression(
raw_license_expression: str,
) -> NormalizedLicenseExpression:
if not raw_license_expression:
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message)
# Pad any parentheses so tokenization can be achieved by merely splitting on
# whitespace.
license_expression = raw_license_expression.replace("(", " ( ").replace(")", " ) ")
licenseref_prefix = "LicenseRef-"
license_refs = {
ref.lower(): "LicenseRef-" + ref[len(licenseref_prefix) :]
for ref in license_expression.split()
if ref.lower().startswith(licenseref_prefix.lower())
}
# Normalize to lower case so we can look up licenses/exceptions
# and so boolean operators are Python-compatible.
license_expression = license_expression.lower()
tokens = license_expression.split()
# Rather than implementing boolean logic, we create an expression that Python can
# parse. Everything that is not involved with the grammar itself is treated as
# `False` and the expression should evaluate as such.
python_tokens = []
for token in tokens:
if token not in {"or", "and", "with", "(", ")"}:
python_tokens.append("False")
elif token == "with":
python_tokens.append("or")
elif token == "(" and python_tokens and python_tokens[-1] not in {"or", "and"}:
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message)
else:
python_tokens.append(token)
python_expression = " ".join(python_tokens)
try:
invalid = eval(python_expression, globals(), locals())
except Exception:
invalid = True
if invalid is not False:
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message) from None
# Take a final pass to check for unknown licenses/exceptions.
normalized_tokens = []
for token in tokens:
if token in {"or", "and", "with", "(", ")"}:
normalized_tokens.append(token.upper())
continue
if normalized_tokens and normalized_tokens[-1] == "WITH":
if token not in EXCEPTIONS:
message = f"Unknown license exception: {token!r}"
raise InvalidLicenseExpression(message)
normalized_tokens.append(EXCEPTIONS[token]["id"])
else:
if token.endswith("+"):
final_token = token[:-1]
suffix = "+"
else:
final_token = token
suffix = ""
if final_token.startswith("licenseref-"):
if not license_ref_allowed.match(final_token):
message = f"Invalid licenseref: {final_token!r}"
raise InvalidLicenseExpression(message)
normalized_tokens.append(license_refs[final_token] + suffix)
else:
if final_token not in LICENSES:
message = f"Unknown license: {final_token!r}"
raise InvalidLicenseExpression(message)
normalized_tokens.append(LICENSES[final_token]["id"] + suffix)
normalized_expression = " ".join(normalized_tokens)
return cast(
NormalizedLicenseExpression,
normalized_expression.replace("( ", "(").replace(" )", ")"),
)
from __future__ import annotations
from typing import TypedDict
class SPDXLicense(TypedDict):
id: str
deprecated: bool
class SPDXException(TypedDict):
id: str
deprecated: bool
VERSION = '3.25.0'
LICENSES: dict[str, SPDXLicense] = {
'0bsd': {'id': '0BSD', 'deprecated': False},
'3d-slicer-1.0': {'id': '3D-Slicer-1.0', 'deprecated': False},
'aal': {'id': 'AAL', 'deprecated': False},
'abstyles': {'id': 'Abstyles', 'deprecated': False},
'adacore-doc': {'id': 'AdaCore-doc', 'deprecated': False},
'adobe-2006': {'id': 'Adobe-2006', 'deprecated': False},
'adobe-display-postscript': {'id': 'Adobe-Display-PostScript', 'deprecated': False},
'adobe-glyph': {'id': 'Adobe-Glyph', 'deprecated': False},
'adobe-utopia': {'id': 'Adobe-Utopia', 'deprecated': False},
'adsl': {'id': 'ADSL', 'deprecated': False},
'afl-1.1': {'id': 'AFL-1.1', 'deprecated': False},
'afl-1.2': {'id': 'AFL-1.2', 'deprecated': False},
'afl-2.0': {'id': 'AFL-2.0', 'deprecated': False},
'afl-2.1': {'id': 'AFL-2.1', 'deprecated': False},
'afl-3.0': {'id': 'AFL-3.0', 'deprecated': False},
'afmparse': {'id': 'Afmparse', 'deprecated': False},
'agpl-1.0': {'id': 'AGPL-1.0', 'deprecated': True},
'agpl-1.0-only': {'id': 'AGPL-1.0-only', 'deprecated': False},
'agpl-1.0-or-later': {'id': 'AGPL-1.0-or-later', 'deprecated': False},
'agpl-3.0': {'id': 'AGPL-3.0', 'deprecated': True},
'agpl-3.0-only': {'id': 'AGPL-3.0-only', 'deprecated': False},
'agpl-3.0-or-later': {'id': 'AGPL-3.0-or-later', 'deprecated': False},
'aladdin': {'id': 'Aladdin', 'deprecated': False},
'amd-newlib': {'id': 'AMD-newlib', 'deprecated': False},
'amdplpa': {'id': 'AMDPLPA', 'deprecated': False},
'aml': {'id': 'AML', 'deprecated': False},
'aml-glslang': {'id': 'AML-glslang', 'deprecated': False},
'ampas': {'id': 'AMPAS', 'deprecated': False},
'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False},
'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False},
'any-osi': {'id': 'any-OSI', 'deprecated': False},
'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False},
'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False},
'apache-2.0': {'id': 'Apache-2.0', 'deprecated': False},
'apafml': {'id': 'APAFML', 'deprecated': False},
'apl-1.0': {'id': 'APL-1.0', 'deprecated': False},
'app-s2p': {'id': 'App-s2p', 'deprecated': False},
'apsl-1.0': {'id': 'APSL-1.0', 'deprecated': False},
'apsl-1.1': {'id': 'APSL-1.1', 'deprecated': False},
'apsl-1.2': {'id': 'APSL-1.2', 'deprecated': False},
'apsl-2.0': {'id': 'APSL-2.0', 'deprecated': False},
'arphic-1999': {'id': 'Arphic-1999', 'deprecated': False},
'artistic-1.0': {'id': 'Artistic-1.0', 'deprecated': False},
'artistic-1.0-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False},
'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False},
'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False},
'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False},
'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False},
'baekmuk': {'id': 'Baekmuk', 'deprecated': False},
'bahyph': {'id': 'Bahyph', 'deprecated': False},
'barr': {'id': 'Barr', 'deprecated': False},
'bcrypt-solar-designer': {'id': 'bcrypt-Solar-Designer', 'deprecated': False},
'beerware': {'id': 'Beerware', 'deprecated': False},
'bitstream-charter': {'id': 'Bitstream-Charter', 'deprecated': False},
'bitstream-vera': {'id': 'Bitstream-Vera', 'deprecated': False},
'bittorrent-1.0': {'id': 'BitTorrent-1.0', 'deprecated': False},
'bittorrent-1.1': {'id': 'BitTorrent-1.1', 'deprecated': False},
'blessing': {'id': 'blessing', 'deprecated': False},
'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False},
'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False},
'borceux': {'id': 'Borceux', 'deprecated': False},
'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False},
'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-Clause', 'deprecated': False},
'bsd-1-clause': {'id': 'BSD-1-Clause', 'deprecated': False},
'bsd-2-clause': {'id': 'BSD-2-Clause', 'deprecated': False},
'bsd-2-clause-darwin': {'id': 'BSD-2-Clause-Darwin', 'deprecated': False},
'bsd-2-clause-first-lines': {'id': 'BSD-2-Clause-first-lines', 'deprecated': False},
'bsd-2-clause-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True},
'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', 'deprecated': True},
'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False},
'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False},
'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False},
'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', 'deprecated': False},
'bsd-3-clause-attribution': {'id': 'BSD-3-Clause-Attribution', 'deprecated': False},
'bsd-3-clause-clear': {'id': 'BSD-3-Clause-Clear', 'deprecated': False},
'bsd-3-clause-flex': {'id': 'BSD-3-Clause-flex', 'deprecated': False},
'bsd-3-clause-hp': {'id': 'BSD-3-Clause-HP', 'deprecated': False},
'bsd-3-clause-lbnl': {'id': 'BSD-3-Clause-LBNL', 'deprecated': False},
'bsd-3-clause-modification': {'id': 'BSD-3-Clause-Modification', 'deprecated': False},
'bsd-3-clause-no-military-license': {'id': 'BSD-3-Clause-No-Military-License', 'deprecated': False},
'bsd-3-clause-no-nuclear-license': {'id': 'BSD-3-Clause-No-Nuclear-License', 'deprecated': False},
'bsd-3-clause-no-nuclear-license-2014': {'id': 'BSD-3-Clause-No-Nuclear-License-2014', 'deprecated': False},
'bsd-3-clause-no-nuclear-warranty': {'id': 'BSD-3-Clause-No-Nuclear-Warranty', 'deprecated': False},
'bsd-3-clause-open-mpi': {'id': 'BSD-3-Clause-Open-MPI', 'deprecated': False},
'bsd-3-clause-sun': {'id': 'BSD-3-Clause-Sun', 'deprecated': False},
'bsd-4-clause': {'id': 'BSD-4-Clause', 'deprecated': False},
'bsd-4-clause-shortened': {'id': 'BSD-4-Clause-Shortened', 'deprecated': False},
'bsd-4-clause-uc': {'id': 'BSD-4-Clause-UC', 'deprecated': False},
'bsd-4.3reno': {'id': 'BSD-4.3RENO', 'deprecated': False},
'bsd-4.3tahoe': {'id': 'BSD-4.3TAHOE', 'deprecated': False},
'bsd-advertising-acknowledgement': {'id': 'BSD-Advertising-Acknowledgement', 'deprecated': False},
'bsd-attribution-hpnd-disclaimer': {'id': 'BSD-Attribution-HPND-disclaimer', 'deprecated': False},
'bsd-inferno-nettverk': {'id': 'BSD-Inferno-Nettverk', 'deprecated': False},
'bsd-protection': {'id': 'BSD-Protection', 'deprecated': False},
'bsd-source-beginning-file': {'id': 'BSD-Source-beginning-file', 'deprecated': False},
'bsd-source-code': {'id': 'BSD-Source-Code', 'deprecated': False},
'bsd-systemics': {'id': 'BSD-Systemics', 'deprecated': False},
'bsd-systemics-w3works': {'id': 'BSD-Systemics-W3Works', 'deprecated': False},
'bsl-1.0': {'id': 'BSL-1.0', 'deprecated': False},
'busl-1.1': {'id': 'BUSL-1.1', 'deprecated': False},
'bzip2-1.0.5': {'id': 'bzip2-1.0.5', 'deprecated': True},
'bzip2-1.0.6': {'id': 'bzip2-1.0.6', 'deprecated': False},
'c-uda-1.0': {'id': 'C-UDA-1.0', 'deprecated': False},
'cal-1.0': {'id': 'CAL-1.0', 'deprecated': False},
'cal-1.0-combined-work-exception': {'id': 'CAL-1.0-Combined-Work-Exception', 'deprecated': False},
'caldera': {'id': 'Caldera', 'deprecated': False},
'caldera-no-preamble': {'id': 'Caldera-no-preamble', 'deprecated': False},
'catharon': {'id': 'Catharon', 'deprecated': False},
'catosl-1.1': {'id': 'CATOSL-1.1', 'deprecated': False},
'cc-by-1.0': {'id': 'CC-BY-1.0', 'deprecated': False},
'cc-by-2.0': {'id': 'CC-BY-2.0', 'deprecated': False},
'cc-by-2.5': {'id': 'CC-BY-2.5', 'deprecated': False},
'cc-by-2.5-au': {'id': 'CC-BY-2.5-AU', 'deprecated': False},
'cc-by-3.0': {'id': 'CC-BY-3.0', 'deprecated': False},
'cc-by-3.0-at': {'id': 'CC-BY-3.0-AT', 'deprecated': False},
'cc-by-3.0-au': {'id': 'CC-BY-3.0-AU', 'deprecated': False},
'cc-by-3.0-de': {'id': 'CC-BY-3.0-DE', 'deprecated': False},
'cc-by-3.0-igo': {'id': 'CC-BY-3.0-IGO', 'deprecated': False},
'cc-by-3.0-nl': {'id': 'CC-BY-3.0-NL', 'deprecated': False},
'cc-by-3.0-us': {'id': 'CC-BY-3.0-US', 'deprecated': False},
'cc-by-4.0': {'id': 'CC-BY-4.0', 'deprecated': False},
'cc-by-nc-1.0': {'id': 'CC-BY-NC-1.0', 'deprecated': False},
'cc-by-nc-2.0': {'id': 'CC-BY-NC-2.0', 'deprecated': False},
'cc-by-nc-2.5': {'id': 'CC-BY-NC-2.5', 'deprecated': False},
'cc-by-nc-3.0': {'id': 'CC-BY-NC-3.0', 'deprecated': False},
'cc-by-nc-3.0-de': {'id': 'CC-BY-NC-3.0-DE', 'deprecated': False},
'cc-by-nc-4.0': {'id': 'CC-BY-NC-4.0', 'deprecated': False},
'cc-by-nc-nd-1.0': {'id': 'CC-BY-NC-ND-1.0', 'deprecated': False},
'cc-by-nc-nd-2.0': {'id': 'CC-BY-NC-ND-2.0', 'deprecated': False},
'cc-by-nc-nd-2.5': {'id': 'CC-BY-NC-ND-2.5', 'deprecated': False},
'cc-by-nc-nd-3.0': {'id': 'CC-BY-NC-ND-3.0', 'deprecated': False},
'cc-by-nc-nd-3.0-de': {'id': 'CC-BY-NC-ND-3.0-DE', 'deprecated': False},
'cc-by-nc-nd-3.0-igo': {'id': 'CC-BY-NC-ND-3.0-IGO', 'deprecated': False},
'cc-by-nc-nd-4.0': {'id': 'CC-BY-NC-ND-4.0', 'deprecated': False},
'cc-by-nc-sa-1.0': {'id': 'CC-BY-NC-SA-1.0', 'deprecated': False},
'cc-by-nc-sa-2.0': {'id': 'CC-BY-NC-SA-2.0', 'deprecated': False},
'cc-by-nc-sa-2.0-de': {'id': 'CC-BY-NC-SA-2.0-DE', 'deprecated': False},
'cc-by-nc-sa-2.0-fr': {'id': 'CC-BY-NC-SA-2.0-FR', 'deprecated': False},
'cc-by-nc-sa-2.0-uk': {'id': 'CC-BY-NC-SA-2.0-UK', 'deprecated': False},
'cc-by-nc-sa-2.5': {'id': 'CC-BY-NC-SA-2.5', 'deprecated': False},
'cc-by-nc-sa-3.0': {'id': 'CC-BY-NC-SA-3.0', 'deprecated': False},
'cc-by-nc-sa-3.0-de': {'id': 'CC-BY-NC-SA-3.0-DE', 'deprecated': False},
'cc-by-nc-sa-3.0-igo': {'id': 'CC-BY-NC-SA-3.0-IGO', 'deprecated': False},
'cc-by-nc-sa-4.0': {'id': 'CC-BY-NC-SA-4.0', 'deprecated': False},
'cc-by-nd-1.0': {'id': 'CC-BY-ND-1.0', 'deprecated': False},
'cc-by-nd-2.0': {'id': 'CC-BY-ND-2.0', 'deprecated': False},
'cc-by-nd-2.5': {'id': 'CC-BY-ND-2.5', 'deprecated': False},
'cc-by-nd-3.0': {'id': 'CC-BY-ND-3.0', 'deprecated': False},
'cc-by-nd-3.0-de': {'id': 'CC-BY-ND-3.0-DE', 'deprecated': False},
'cc-by-nd-4.0': {'id': 'CC-BY-ND-4.0', 'deprecated': False},
'cc-by-sa-1.0': {'id': 'CC-BY-SA-1.0', 'deprecated': False},
'cc-by-sa-2.0': {'id': 'CC-BY-SA-2.0', 'deprecated': False},
'cc-by-sa-2.0-uk': {'id': 'CC-BY-SA-2.0-UK', 'deprecated': False},
'cc-by-sa-2.1-jp': {'id': 'CC-BY-SA-2.1-JP', 'deprecated': False},
'cc-by-sa-2.5': {'id': 'CC-BY-SA-2.5', 'deprecated': False},
'cc-by-sa-3.0': {'id': 'CC-BY-SA-3.0', 'deprecated': False},
'cc-by-sa-3.0-at': {'id': 'CC-BY-SA-3.0-AT', 'deprecated': False},
'cc-by-sa-3.0-de': {'id': 'CC-BY-SA-3.0-DE', 'deprecated': False},
'cc-by-sa-3.0-igo': {'id': 'CC-BY-SA-3.0-IGO', 'deprecated': False},
'cc-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False},
'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False},
'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False},
'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False},
'cddl-1.1': {'id': 'CDDL-1.1', 'deprecated': False},
'cdl-1.0': {'id': 'CDL-1.0', 'deprecated': False},
'cdla-permissive-1.0': {'id': 'CDLA-Permissive-1.0', 'deprecated': False},
'cdla-permissive-2.0': {'id': 'CDLA-Permissive-2.0', 'deprecated': False},
'cdla-sharing-1.0': {'id': 'CDLA-Sharing-1.0', 'deprecated': False},
'cecill-1.0': {'id': 'CECILL-1.0', 'deprecated': False},
'cecill-1.1': {'id': 'CECILL-1.1', 'deprecated': False},
'cecill-2.0': {'id': 'CECILL-2.0', 'deprecated': False},
'cecill-2.1': {'id': 'CECILL-2.1', 'deprecated': False},
'cecill-b': {'id': 'CECILL-B', 'deprecated': False},
'cecill-c': {'id': 'CECILL-C', 'deprecated': False},
'cern-ohl-1.1': {'id': 'CERN-OHL-1.1', 'deprecated': False},
'cern-ohl-1.2': {'id': 'CERN-OHL-1.2', 'deprecated': False},
'cern-ohl-p-2.0': {'id': 'CERN-OHL-P-2.0', 'deprecated': False},
'cern-ohl-s-2.0': {'id': 'CERN-OHL-S-2.0', 'deprecated': False},
'cern-ohl-w-2.0': {'id': 'CERN-OHL-W-2.0', 'deprecated': False},
'cfitsio': {'id': 'CFITSIO', 'deprecated': False},
'check-cvs': {'id': 'check-cvs', 'deprecated': False},
'checkmk': {'id': 'checkmk', 'deprecated': False},
'clartistic': {'id': 'ClArtistic', 'deprecated': False},
'clips': {'id': 'Clips', 'deprecated': False},
'cmu-mach': {'id': 'CMU-Mach', 'deprecated': False},
'cmu-mach-nodoc': {'id': 'CMU-Mach-nodoc', 'deprecated': False},
'cnri-jython': {'id': 'CNRI-Jython', 'deprecated': False},
'cnri-python': {'id': 'CNRI-Python', 'deprecated': False},
'cnri-python-gpl-compatible': {'id': 'CNRI-Python-GPL-Compatible', 'deprecated': False},
'coil-1.0': {'id': 'COIL-1.0', 'deprecated': False},
'community-spec-1.0': {'id': 'Community-Spec-1.0', 'deprecated': False},
'condor-1.1': {'id': 'Condor-1.1', 'deprecated': False},
'copyleft-next-0.3.0': {'id': 'copyleft-next-0.3.0', 'deprecated': False},
'copyleft-next-0.3.1': {'id': 'copyleft-next-0.3.1', 'deprecated': False},
'cornell-lossless-jpeg': {'id': 'Cornell-Lossless-JPEG', 'deprecated': False},
'cpal-1.0': {'id': 'CPAL-1.0', 'deprecated': False},
'cpl-1.0': {'id': 'CPL-1.0', 'deprecated': False},
'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False},
'cronyx': {'id': 'Cronyx', 'deprecated': False},
'crossword': {'id': 'Crossword', 'deprecated': False},
'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False},
'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False},
'cube': {'id': 'Cube', 'deprecated': False},
'curl': {'id': 'curl', 'deprecated': False},
'cve-tou': {'id': 'cve-tou', 'deprecated': False},
'd-fsl-1.0': {'id': 'D-FSL-1.0', 'deprecated': False},
'dec-3-clause': {'id': 'DEC-3-Clause', 'deprecated': False},
'diffmark': {'id': 'diffmark', 'deprecated': False},
'dl-de-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False},
'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False},
'doc': {'id': 'DOC', 'deprecated': False},
'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False},
'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False},
'dotseqn': {'id': 'Dotseqn', 'deprecated': False},
'drl-1.0': {'id': 'DRL-1.0', 'deprecated': False},
'drl-1.1': {'id': 'DRL-1.1', 'deprecated': False},
'dsdp': {'id': 'DSDP', 'deprecated': False},
'dtoa': {'id': 'dtoa', 'deprecated': False},
'dvipdfm': {'id': 'dvipdfm', 'deprecated': False},
'ecl-1.0': {'id': 'ECL-1.0', 'deprecated': False},
'ecl-2.0': {'id': 'ECL-2.0', 'deprecated': False},
'ecos-2.0': {'id': 'eCos-2.0', 'deprecated': True},
'efl-1.0': {'id': 'EFL-1.0', 'deprecated': False},
'efl-2.0': {'id': 'EFL-2.0', 'deprecated': False},
'egenix': {'id': 'eGenix', 'deprecated': False},
'elastic-2.0': {'id': 'Elastic-2.0', 'deprecated': False},
'entessa': {'id': 'Entessa', 'deprecated': False},
'epics': {'id': 'EPICS', 'deprecated': False},
'epl-1.0': {'id': 'EPL-1.0', 'deprecated': False},
'epl-2.0': {'id': 'EPL-2.0', 'deprecated': False},
'erlpl-1.1': {'id': 'ErlPL-1.1', 'deprecated': False},
'etalab-2.0': {'id': 'etalab-2.0', 'deprecated': False},
'eudatagrid': {'id': 'EUDatagrid', 'deprecated': False},
'eupl-1.0': {'id': 'EUPL-1.0', 'deprecated': False},
'eupl-1.1': {'id': 'EUPL-1.1', 'deprecated': False},
'eupl-1.2': {'id': 'EUPL-1.2', 'deprecated': False},
'eurosym': {'id': 'Eurosym', 'deprecated': False},
'fair': {'id': 'Fair', 'deprecated': False},
'fbm': {'id': 'FBM', 'deprecated': False},
'fdk-aac': {'id': 'FDK-AAC', 'deprecated': False},
'ferguson-twofish': {'id': 'Ferguson-Twofish', 'deprecated': False},
'frameworx-1.0': {'id': 'Frameworx-1.0', 'deprecated': False},
'freebsd-doc': {'id': 'FreeBSD-DOC', 'deprecated': False},
'freeimage': {'id': 'FreeImage', 'deprecated': False},
'fsfap': {'id': 'FSFAP', 'deprecated': False},
'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False},
'fsful': {'id': 'FSFUL', 'deprecated': False},
'fsfullr': {'id': 'FSFULLR', 'deprecated': False},
'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False},
'ftl': {'id': 'FTL', 'deprecated': False},
'furuseth': {'id': 'Furuseth', 'deprecated': False},
'fwlw': {'id': 'fwlw', 'deprecated': False},
'gcr-docs': {'id': 'GCR-docs', 'deprecated': False},
'gd': {'id': 'GD', 'deprecated': False},
'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True},
'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False},
'gfdl-1.1-invariants-or-later': {'id': 'GFDL-1.1-invariants-or-later', 'deprecated': False},
'gfdl-1.1-no-invariants-only': {'id': 'GFDL-1.1-no-invariants-only', 'deprecated': False},
'gfdl-1.1-no-invariants-or-later': {'id': 'GFDL-1.1-no-invariants-or-later', 'deprecated': False},
'gfdl-1.1-only': {'id': 'GFDL-1.1-only', 'deprecated': False},
'gfdl-1.1-or-later': {'id': 'GFDL-1.1-or-later', 'deprecated': False},
'gfdl-1.2': {'id': 'GFDL-1.2', 'deprecated': True},
'gfdl-1.2-invariants-only': {'id': 'GFDL-1.2-invariants-only', 'deprecated': False},
'gfdl-1.2-invariants-or-later': {'id': 'GFDL-1.2-invariants-or-later', 'deprecated': False},
'gfdl-1.2-no-invariants-only': {'id': 'GFDL-1.2-no-invariants-only', 'deprecated': False},
'gfdl-1.2-no-invariants-or-later': {'id': 'GFDL-1.2-no-invariants-or-later', 'deprecated': False},
'gfdl-1.2-only': {'id': 'GFDL-1.2-only', 'deprecated': False},
'gfdl-1.2-or-later': {'id': 'GFDL-1.2-or-later', 'deprecated': False},
'gfdl-1.3': {'id': 'GFDL-1.3', 'deprecated': True},
'gfdl-1.3-invariants-only': {'id': 'GFDL-1.3-invariants-only', 'deprecated': False},
'gfdl-1.3-invariants-or-later': {'id': 'GFDL-1.3-invariants-or-later', 'deprecated': False},
'gfdl-1.3-no-invariants-only': {'id': 'GFDL-1.3-no-invariants-only', 'deprecated': False},
'gfdl-1.3-no-invariants-or-later': {'id': 'GFDL-1.3-no-invariants-or-later', 'deprecated': False},
'gfdl-1.3-only': {'id': 'GFDL-1.3-only', 'deprecated': False},
'gfdl-1.3-or-later': {'id': 'GFDL-1.3-or-later', 'deprecated': False},
'giftware': {'id': 'Giftware', 'deprecated': False},
'gl2ps': {'id': 'GL2PS', 'deprecated': False},
'glide': {'id': 'Glide', 'deprecated': False},
'glulxe': {'id': 'Glulxe', 'deprecated': False},
'glwtpl': {'id': 'GLWTPL', 'deprecated': False},
'gnuplot': {'id': 'gnuplot', 'deprecated': False},
'gpl-1.0': {'id': 'GPL-1.0', 'deprecated': True},
'gpl-1.0+': {'id': 'GPL-1.0+', 'deprecated': True},
'gpl-1.0-only': {'id': 'GPL-1.0-only', 'deprecated': False},
'gpl-1.0-or-later': {'id': 'GPL-1.0-or-later', 'deprecated': False},
'gpl-2.0': {'id': 'GPL-2.0', 'deprecated': True},
'gpl-2.0+': {'id': 'GPL-2.0+', 'deprecated': True},
'gpl-2.0-only': {'id': 'GPL-2.0-only', 'deprecated': False},
'gpl-2.0-or-later': {'id': 'GPL-2.0-or-later', 'deprecated': False},
'gpl-2.0-with-autoconf-exception': {'id': 'GPL-2.0-with-autoconf-exception', 'deprecated': True},
'gpl-2.0-with-bison-exception': {'id': 'GPL-2.0-with-bison-exception', 'deprecated': True},
'gpl-2.0-with-classpath-exception': {'id': 'GPL-2.0-with-classpath-exception', 'deprecated': True},
'gpl-2.0-with-font-exception': {'id': 'GPL-2.0-with-font-exception', 'deprecated': True},
'gpl-2.0-with-gcc-exception': {'id': 'GPL-2.0-with-GCC-exception', 'deprecated': True},
'gpl-3.0': {'id': 'GPL-3.0', 'deprecated': True},
'gpl-3.0+': {'id': 'GPL-3.0+', 'deprecated': True},
'gpl-3.0-only': {'id': 'GPL-3.0-only', 'deprecated': False},
'gpl-3.0-or-later': {'id': 'GPL-3.0-or-later', 'deprecated': False},
'gpl-3.0-with-autoconf-exception': {'id': 'GPL-3.0-with-autoconf-exception', 'deprecated': True},
'gpl-3.0-with-gcc-exception': {'id': 'GPL-3.0-with-GCC-exception', 'deprecated': True},
'graphics-gems': {'id': 'Graphics-Gems', 'deprecated': False},
'gsoap-1.3b': {'id': 'gSOAP-1.3b', 'deprecated': False},
'gtkbook': {'id': 'gtkbook', 'deprecated': False},
'gutmann': {'id': 'Gutmann', 'deprecated': False},
'haskellreport': {'id': 'HaskellReport', 'deprecated': False},
'hdparm': {'id': 'hdparm', 'deprecated': False},
'hidapi': {'id': 'HIDAPI', 'deprecated': False},
'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False},
'hp-1986': {'id': 'HP-1986', 'deprecated': False},
'hp-1989': {'id': 'HP-1989', 'deprecated': False},
'hpnd': {'id': 'HPND', 'deprecated': False},
'hpnd-dec': {'id': 'HPND-DEC', 'deprecated': False},
'hpnd-doc': {'id': 'HPND-doc', 'deprecated': False},
'hpnd-doc-sell': {'id': 'HPND-doc-sell', 'deprecated': False},
'hpnd-export-us': {'id': 'HPND-export-US', 'deprecated': False},
'hpnd-export-us-acknowledgement': {'id': 'HPND-export-US-acknowledgement', 'deprecated': False},
'hpnd-export-us-modify': {'id': 'HPND-export-US-modify', 'deprecated': False},
'hpnd-export2-us': {'id': 'HPND-export2-US', 'deprecated': False},
'hpnd-fenneberg-livingston': {'id': 'HPND-Fenneberg-Livingston', 'deprecated': False},
'hpnd-inria-imag': {'id': 'HPND-INRIA-IMAG', 'deprecated': False},
'hpnd-intel': {'id': 'HPND-Intel', 'deprecated': False},
'hpnd-kevlin-henney': {'id': 'HPND-Kevlin-Henney', 'deprecated': False},
'hpnd-markus-kuhn': {'id': 'HPND-Markus-Kuhn', 'deprecated': False},
'hpnd-merchantability-variant': {'id': 'HPND-merchantability-variant', 'deprecated': False},
'hpnd-mit-disclaimer': {'id': 'HPND-MIT-disclaimer', 'deprecated': False},
'hpnd-netrek': {'id': 'HPND-Netrek', 'deprecated': False},
'hpnd-pbmplus': {'id': 'HPND-Pbmplus', 'deprecated': False},
'hpnd-sell-mit-disclaimer-xserver': {'id': 'HPND-sell-MIT-disclaimer-xserver', 'deprecated': False},
'hpnd-sell-regexpr': {'id': 'HPND-sell-regexpr', 'deprecated': False},
'hpnd-sell-variant': {'id': 'HPND-sell-variant', 'deprecated': False},
'hpnd-sell-variant-mit-disclaimer': {'id': 'HPND-sell-variant-MIT-disclaimer', 'deprecated': False},
'hpnd-sell-variant-mit-disclaimer-rev': {'id': 'HPND-sell-variant-MIT-disclaimer-rev', 'deprecated': False},
'hpnd-uc': {'id': 'HPND-UC', 'deprecated': False},
'hpnd-uc-export-us': {'id': 'HPND-UC-export-US', 'deprecated': False},
'htmltidy': {'id': 'HTMLTIDY', 'deprecated': False},
'ibm-pibs': {'id': 'IBM-pibs', 'deprecated': False},
'icu': {'id': 'ICU', 'deprecated': False},
'iec-code-components-eula': {'id': 'IEC-Code-Components-EULA', 'deprecated': False},
'ijg': {'id': 'IJG', 'deprecated': False},
'ijg-short': {'id': 'IJG-short', 'deprecated': False},
'imagemagick': {'id': 'ImageMagick', 'deprecated': False},
'imatix': {'id': 'iMatix', 'deprecated': False},
'imlib2': {'id': 'Imlib2', 'deprecated': False},
'info-zip': {'id': 'Info-ZIP', 'deprecated': False},
'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False},
'intel': {'id': 'Intel', 'deprecated': False},
'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False},
'interbase-1.0': {'id': 'Interbase-1.0', 'deprecated': False},
'ipa': {'id': 'IPA', 'deprecated': False},
'ipl-1.0': {'id': 'IPL-1.0', 'deprecated': False},
'isc': {'id': 'ISC', 'deprecated': False},
'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False},
'jam': {'id': 'Jam', 'deprecated': False},
'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False},
'jpl-image': {'id': 'JPL-image', 'deprecated': False},
'jpnic': {'id': 'JPNIC', 'deprecated': False},
'json': {'id': 'JSON', 'deprecated': False},
'kastrup': {'id': 'Kastrup', 'deprecated': False},
'kazlib': {'id': 'Kazlib', 'deprecated': False},
'knuth-ctan': {'id': 'Knuth-CTAN', 'deprecated': False},
'lal-1.2': {'id': 'LAL-1.2', 'deprecated': False},
'lal-1.3': {'id': 'LAL-1.3', 'deprecated': False},
'latex2e': {'id': 'Latex2e', 'deprecated': False},
'latex2e-translated-notice': {'id': 'Latex2e-translated-notice', 'deprecated': False},
'leptonica': {'id': 'Leptonica', 'deprecated': False},
'lgpl-2.0': {'id': 'LGPL-2.0', 'deprecated': True},
'lgpl-2.0+': {'id': 'LGPL-2.0+', 'deprecated': True},
'lgpl-2.0-only': {'id': 'LGPL-2.0-only', 'deprecated': False},
'lgpl-2.0-or-later': {'id': 'LGPL-2.0-or-later', 'deprecated': False},
'lgpl-2.1': {'id': 'LGPL-2.1', 'deprecated': True},
'lgpl-2.1+': {'id': 'LGPL-2.1+', 'deprecated': True},
'lgpl-2.1-only': {'id': 'LGPL-2.1-only', 'deprecated': False},
'lgpl-2.1-or-later': {'id': 'LGPL-2.1-or-later', 'deprecated': False},
'lgpl-3.0': {'id': 'LGPL-3.0', 'deprecated': True},
'lgpl-3.0+': {'id': 'LGPL-3.0+', 'deprecated': True},
'lgpl-3.0-only': {'id': 'LGPL-3.0-only', 'deprecated': False},
'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False},
'lgpllr': {'id': 'LGPLLR', 'deprecated': False},
'libpng': {'id': 'Libpng', 'deprecated': False},
'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False},
'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False},
'libtiff': {'id': 'libtiff', 'deprecated': False},
'libutil-david-nugent': {'id': 'libutil-David-Nugent', 'deprecated': False},
'liliq-p-1.1': {'id': 'LiLiQ-P-1.1', 'deprecated': False},
'liliq-r-1.1': {'id': 'LiLiQ-R-1.1', 'deprecated': False},
'liliq-rplus-1.1': {'id': 'LiLiQ-Rplus-1.1', 'deprecated': False},
'linux-man-pages-1-para': {'id': 'Linux-man-pages-1-para', 'deprecated': False},
'linux-man-pages-copyleft': {'id': 'Linux-man-pages-copyleft', 'deprecated': False},
'linux-man-pages-copyleft-2-para': {'id': 'Linux-man-pages-copyleft-2-para', 'deprecated': False},
'linux-man-pages-copyleft-var': {'id': 'Linux-man-pages-copyleft-var', 'deprecated': False},
'linux-openib': {'id': 'Linux-OpenIB', 'deprecated': False},
'loop': {'id': 'LOOP', 'deprecated': False},
'lpd-document': {'id': 'LPD-document', 'deprecated': False},
'lpl-1.0': {'id': 'LPL-1.0', 'deprecated': False},
'lpl-1.02': {'id': 'LPL-1.02', 'deprecated': False},
'lppl-1.0': {'id': 'LPPL-1.0', 'deprecated': False},
'lppl-1.1': {'id': 'LPPL-1.1', 'deprecated': False},
'lppl-1.2': {'id': 'LPPL-1.2', 'deprecated': False},
'lppl-1.3a': {'id': 'LPPL-1.3a', 'deprecated': False},
'lppl-1.3c': {'id': 'LPPL-1.3c', 'deprecated': False},
'lsof': {'id': 'lsof', 'deprecated': False},
'lucida-bitmap-fonts': {'id': 'Lucida-Bitmap-Fonts', 'deprecated': False},
'lzma-sdk-9.11-to-9.20': {'id': 'LZMA-SDK-9.11-to-9.20', 'deprecated': False},
'lzma-sdk-9.22': {'id': 'LZMA-SDK-9.22', 'deprecated': False},
'mackerras-3-clause': {'id': 'Mackerras-3-Clause', 'deprecated': False},
'mackerras-3-clause-acknowledgment': {'id': 'Mackerras-3-Clause-acknowledgment', 'deprecated': False},
'magaz': {'id': 'magaz', 'deprecated': False},
'mailprio': {'id': 'mailprio', 'deprecated': False},
'makeindex': {'id': 'MakeIndex', 'deprecated': False},
'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False},
'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False},
'metamail': {'id': 'metamail', 'deprecated': False},
'minpack': {'id': 'Minpack', 'deprecated': False},
'miros': {'id': 'MirOS', 'deprecated': False},
'mit': {'id': 'MIT', 'deprecated': False},
'mit-0': {'id': 'MIT-0', 'deprecated': False},
'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False},
'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False},
'mit-enna': {'id': 'MIT-enna', 'deprecated': False},
'mit-feh': {'id': 'MIT-feh', 'deprecated': False},
'mit-festival': {'id': 'MIT-Festival', 'deprecated': False},
'mit-khronos-old': {'id': 'MIT-Khronos-old', 'deprecated': False},
'mit-modern-variant': {'id': 'MIT-Modern-Variant', 'deprecated': False},
'mit-open-group': {'id': 'MIT-open-group', 'deprecated': False},
'mit-testregex': {'id': 'MIT-testregex', 'deprecated': False},
'mit-wu': {'id': 'MIT-Wu', 'deprecated': False},
'mitnfa': {'id': 'MITNFA', 'deprecated': False},
'mmixware': {'id': 'MMIXware', 'deprecated': False},
'motosoto': {'id': 'Motosoto', 'deprecated': False},
'mpeg-ssg': {'id': 'MPEG-SSG', 'deprecated': False},
'mpi-permissive': {'id': 'mpi-permissive', 'deprecated': False},
'mpich2': {'id': 'mpich2', 'deprecated': False},
'mpl-1.0': {'id': 'MPL-1.0', 'deprecated': False},
'mpl-1.1': {'id': 'MPL-1.1', 'deprecated': False},
'mpl-2.0': {'id': 'MPL-2.0', 'deprecated': False},
'mpl-2.0-no-copyleft-exception': {'id': 'MPL-2.0-no-copyleft-exception', 'deprecated': False},
'mplus': {'id': 'mplus', 'deprecated': False},
'ms-lpl': {'id': 'MS-LPL', 'deprecated': False},
'ms-pl': {'id': 'MS-PL', 'deprecated': False},
'ms-rl': {'id': 'MS-RL', 'deprecated': False},
'mtll': {'id': 'MTLL', 'deprecated': False},
'mulanpsl-1.0': {'id': 'MulanPSL-1.0', 'deprecated': False},
'mulanpsl-2.0': {'id': 'MulanPSL-2.0', 'deprecated': False},
'multics': {'id': 'Multics', 'deprecated': False},
'mup': {'id': 'Mup', 'deprecated': False},
'naist-2003': {'id': 'NAIST-2003', 'deprecated': False},
'nasa-1.3': {'id': 'NASA-1.3', 'deprecated': False},
'naumen': {'id': 'Naumen', 'deprecated': False},
'nbpl-1.0': {'id': 'NBPL-1.0', 'deprecated': False},
'ncbi-pd': {'id': 'NCBI-PD', 'deprecated': False},
'ncgl-uk-2.0': {'id': 'NCGL-UK-2.0', 'deprecated': False},
'ncl': {'id': 'NCL', 'deprecated': False},
'ncsa': {'id': 'NCSA', 'deprecated': False},
'net-snmp': {'id': 'Net-SNMP', 'deprecated': True},
'netcdf': {'id': 'NetCDF', 'deprecated': False},
'newsletr': {'id': 'Newsletr', 'deprecated': False},
'ngpl': {'id': 'NGPL', 'deprecated': False},
'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False},
'nist-pd': {'id': 'NIST-PD', 'deprecated': False},
'nist-pd-fallback': {'id': 'NIST-PD-fallback', 'deprecated': False},
'nist-software': {'id': 'NIST-Software', 'deprecated': False},
'nlod-1.0': {'id': 'NLOD-1.0', 'deprecated': False},
'nlod-2.0': {'id': 'NLOD-2.0', 'deprecated': False},
'nlpl': {'id': 'NLPL', 'deprecated': False},
'nokia': {'id': 'Nokia', 'deprecated': False},
'nosl': {'id': 'NOSL', 'deprecated': False},
'noweb': {'id': 'Noweb', 'deprecated': False},
'npl-1.0': {'id': 'NPL-1.0', 'deprecated': False},
'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False},
'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False},
'nrl': {'id': 'NRL', 'deprecated': False},
'ntp': {'id': 'NTP', 'deprecated': False},
'ntp-0': {'id': 'NTP-0', 'deprecated': False},
'nunit': {'id': 'Nunit', 'deprecated': True},
'o-uda-1.0': {'id': 'O-UDA-1.0', 'deprecated': False},
'oar': {'id': 'OAR', 'deprecated': False},
'occt-pl': {'id': 'OCCT-PL', 'deprecated': False},
'oclc-2.0': {'id': 'OCLC-2.0', 'deprecated': False},
'odbl-1.0': {'id': 'ODbL-1.0', 'deprecated': False},
'odc-by-1.0': {'id': 'ODC-By-1.0', 'deprecated': False},
'offis': {'id': 'OFFIS', 'deprecated': False},
'ofl-1.0': {'id': 'OFL-1.0', 'deprecated': False},
'ofl-1.0-no-rfn': {'id': 'OFL-1.0-no-RFN', 'deprecated': False},
'ofl-1.0-rfn': {'id': 'OFL-1.0-RFN', 'deprecated': False},
'ofl-1.1': {'id': 'OFL-1.1', 'deprecated': False},
'ofl-1.1-no-rfn': {'id': 'OFL-1.1-no-RFN', 'deprecated': False},
'ofl-1.1-rfn': {'id': 'OFL-1.1-RFN', 'deprecated': False},
'ogc-1.0': {'id': 'OGC-1.0', 'deprecated': False},
'ogdl-taiwan-1.0': {'id': 'OGDL-Taiwan-1.0', 'deprecated': False},
'ogl-canada-2.0': {'id': 'OGL-Canada-2.0', 'deprecated': False},
'ogl-uk-1.0': {'id': 'OGL-UK-1.0', 'deprecated': False},
'ogl-uk-2.0': {'id': 'OGL-UK-2.0', 'deprecated': False},
'ogl-uk-3.0': {'id': 'OGL-UK-3.0', 'deprecated': False},
'ogtsl': {'id': 'OGTSL', 'deprecated': False},
'oldap-1.1': {'id': 'OLDAP-1.1', 'deprecated': False},
'oldap-1.2': {'id': 'OLDAP-1.2', 'deprecated': False},
'oldap-1.3': {'id': 'OLDAP-1.3', 'deprecated': False},
'oldap-1.4': {'id': 'OLDAP-1.4', 'deprecated': False},
'oldap-2.0': {'id': 'OLDAP-2.0', 'deprecated': False},
'oldap-2.0.1': {'id': 'OLDAP-2.0.1', 'deprecated': False},
'oldap-2.1': {'id': 'OLDAP-2.1', 'deprecated': False},
'oldap-2.2': {'id': 'OLDAP-2.2', 'deprecated': False},
'oldap-2.2.1': {'id': 'OLDAP-2.2.1', 'deprecated': False},
'oldap-2.2.2': {'id': 'OLDAP-2.2.2', 'deprecated': False},
'oldap-2.3': {'id': 'OLDAP-2.3', 'deprecated': False},
'oldap-2.4': {'id': 'OLDAP-2.4', 'deprecated': False},
'oldap-2.5': {'id': 'OLDAP-2.5', 'deprecated': False},
'oldap-2.6': {'id': 'OLDAP-2.6', 'deprecated': False},
'oldap-2.7': {'id': 'OLDAP-2.7', 'deprecated': False},
'oldap-2.8': {'id': 'OLDAP-2.8', 'deprecated': False},
'olfl-1.3': {'id': 'OLFL-1.3', 'deprecated': False},
'oml': {'id': 'OML', 'deprecated': False},
'openpbs-2.3': {'id': 'OpenPBS-2.3', 'deprecated': False},
'openssl': {'id': 'OpenSSL', 'deprecated': False},
'openssl-standalone': {'id': 'OpenSSL-standalone', 'deprecated': False},
'openvision': {'id': 'OpenVision', 'deprecated': False},
'opl-1.0': {'id': 'OPL-1.0', 'deprecated': False},
'opl-uk-3.0': {'id': 'OPL-UK-3.0', 'deprecated': False},
'opubl-1.0': {'id': 'OPUBL-1.0', 'deprecated': False},
'oset-pl-2.1': {'id': 'OSET-PL-2.1', 'deprecated': False},
'osl-1.0': {'id': 'OSL-1.0', 'deprecated': False},
'osl-1.1': {'id': 'OSL-1.1', 'deprecated': False},
'osl-2.0': {'id': 'OSL-2.0', 'deprecated': False},
'osl-2.1': {'id': 'OSL-2.1', 'deprecated': False},
'osl-3.0': {'id': 'OSL-3.0', 'deprecated': False},
'padl': {'id': 'PADL', 'deprecated': False},
'parity-6.0.0': {'id': 'Parity-6.0.0', 'deprecated': False},
'parity-7.0.0': {'id': 'Parity-7.0.0', 'deprecated': False},
'pddl-1.0': {'id': 'PDDL-1.0', 'deprecated': False},
'php-3.0': {'id': 'PHP-3.0', 'deprecated': False},
'php-3.01': {'id': 'PHP-3.01', 'deprecated': False},
'pixar': {'id': 'Pixar', 'deprecated': False},
'pkgconf': {'id': 'pkgconf', 'deprecated': False},
'plexus': {'id': 'Plexus', 'deprecated': False},
'pnmstitch': {'id': 'pnmstitch', 'deprecated': False},
'polyform-noncommercial-1.0.0': {'id': 'PolyForm-Noncommercial-1.0.0', 'deprecated': False},
'polyform-small-business-1.0.0': {'id': 'PolyForm-Small-Business-1.0.0', 'deprecated': False},
'postgresql': {'id': 'PostgreSQL', 'deprecated': False},
'ppl': {'id': 'PPL', 'deprecated': False},
'psf-2.0': {'id': 'PSF-2.0', 'deprecated': False},
'psfrag': {'id': 'psfrag', 'deprecated': False},
'psutils': {'id': 'psutils', 'deprecated': False},
'python-2.0': {'id': 'Python-2.0', 'deprecated': False},
'python-2.0.1': {'id': 'Python-2.0.1', 'deprecated': False},
'python-ldap': {'id': 'python-ldap', 'deprecated': False},
'qhull': {'id': 'Qhull', 'deprecated': False},
'qpl-1.0': {'id': 'QPL-1.0', 'deprecated': False},
'qpl-1.0-inria-2004': {'id': 'QPL-1.0-INRIA-2004', 'deprecated': False},
'radvd': {'id': 'radvd', 'deprecated': False},
'rdisc': {'id': 'Rdisc', 'deprecated': False},
'rhecos-1.1': {'id': 'RHeCos-1.1', 'deprecated': False},
'rpl-1.1': {'id': 'RPL-1.1', 'deprecated': False},
'rpl-1.5': {'id': 'RPL-1.5', 'deprecated': False},
'rpsl-1.0': {'id': 'RPSL-1.0', 'deprecated': False},
'rsa-md': {'id': 'RSA-MD', 'deprecated': False},
'rscpl': {'id': 'RSCPL', 'deprecated': False},
'ruby': {'id': 'Ruby', 'deprecated': False},
'ruby-pty': {'id': 'Ruby-pty', 'deprecated': False},
'sax-pd': {'id': 'SAX-PD', 'deprecated': False},
'sax-pd-2.0': {'id': 'SAX-PD-2.0', 'deprecated': False},
'saxpath': {'id': 'Saxpath', 'deprecated': False},
'scea': {'id': 'SCEA', 'deprecated': False},
'schemereport': {'id': 'SchemeReport', 'deprecated': False},
'sendmail': {'id': 'Sendmail', 'deprecated': False},
'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False},
'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False},
'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False},
'sgi-b-2.0': {'id': 'SGI-B-2.0', 'deprecated': False},
'sgi-opengl': {'id': 'SGI-OpenGL', 'deprecated': False},
'sgp4': {'id': 'SGP4', 'deprecated': False},
'shl-0.5': {'id': 'SHL-0.5', 'deprecated': False},
'shl-0.51': {'id': 'SHL-0.51', 'deprecated': False},
'simpl-2.0': {'id': 'SimPL-2.0', 'deprecated': False},
'sissl': {'id': 'SISSL', 'deprecated': False},
'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False},
'sl': {'id': 'SL', 'deprecated': False},
'sleepycat': {'id': 'Sleepycat', 'deprecated': False},
'smlnj': {'id': 'SMLNJ', 'deprecated': False},
'smppl': {'id': 'SMPPL', 'deprecated': False},
'snia': {'id': 'SNIA', 'deprecated': False},
'snprintf': {'id': 'snprintf', 'deprecated': False},
'softsurfer': {'id': 'softSurfer', 'deprecated': False},
'soundex': {'id': 'Soundex', 'deprecated': False},
'spencer-86': {'id': 'Spencer-86', 'deprecated': False},
'spencer-94': {'id': 'Spencer-94', 'deprecated': False},
'spencer-99': {'id': 'Spencer-99', 'deprecated': False},
'spl-1.0': {'id': 'SPL-1.0', 'deprecated': False},
'ssh-keyscan': {'id': 'ssh-keyscan', 'deprecated': False},
'ssh-openssh': {'id': 'SSH-OpenSSH', 'deprecated': False},
'ssh-short': {'id': 'SSH-short', 'deprecated': False},
'ssleay-standalone': {'id': 'SSLeay-standalone', 'deprecated': False},
'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False},
'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True},
'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False},
'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False},
'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False},
'sunpro': {'id': 'SunPro', 'deprecated': False},
'swl': {'id': 'SWL', 'deprecated': False},
'swrule': {'id': 'swrule', 'deprecated': False},
'symlinks': {'id': 'Symlinks', 'deprecated': False},
'tapr-ohl-1.0': {'id': 'TAPR-OHL-1.0', 'deprecated': False},
'tcl': {'id': 'TCL', 'deprecated': False},
'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False},
'termreadkey': {'id': 'TermReadKey', 'deprecated': False},
'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False},
'threeparttable': {'id': 'threeparttable', 'deprecated': False},
'tmate': {'id': 'TMate', 'deprecated': False},
'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False},
'tosl': {'id': 'TOSL', 'deprecated': False},
'tpdl': {'id': 'TPDL', 'deprecated': False},
'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False},
'ttwl': {'id': 'TTWL', 'deprecated': False},
'ttyp0': {'id': 'TTYP0', 'deprecated': False},
'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', 'deprecated': False},
'tu-berlin-2.0': {'id': 'TU-Berlin-2.0', 'deprecated': False},
'ubuntu-font-1.0': {'id': 'Ubuntu-font-1.0', 'deprecated': False},
'ucar': {'id': 'UCAR', 'deprecated': False},
'ucl-1.0': {'id': 'UCL-1.0', 'deprecated': False},
'ulem': {'id': 'ulem', 'deprecated': False},
'umich-merit': {'id': 'UMich-Merit', 'deprecated': False},
'unicode-3.0': {'id': 'Unicode-3.0', 'deprecated': False},
'unicode-dfs-2015': {'id': 'Unicode-DFS-2015', 'deprecated': False},
'unicode-dfs-2016': {'id': 'Unicode-DFS-2016', 'deprecated': False},
'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False},
'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False},
'unlicense': {'id': 'Unlicense', 'deprecated': False},
'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False},
'urt-rle': {'id': 'URT-RLE', 'deprecated': False},
'vim': {'id': 'Vim', 'deprecated': False},
'vostrom': {'id': 'VOSTROM', 'deprecated': False},
'vsl-1.0': {'id': 'VSL-1.0', 'deprecated': False},
'w3c': {'id': 'W3C', 'deprecated': False},
'w3c-19980720': {'id': 'W3C-19980720', 'deprecated': False},
'w3c-20150513': {'id': 'W3C-20150513', 'deprecated': False},
'w3m': {'id': 'w3m', 'deprecated': False},
'watcom-1.0': {'id': 'Watcom-1.0', 'deprecated': False},
'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False},
'wsuipa': {'id': 'Wsuipa', 'deprecated': False},
'wtfpl': {'id': 'WTFPL', 'deprecated': False},
'wxwindows': {'id': 'wxWindows', 'deprecated': True},
'x11': {'id': 'X11', 'deprecated': False},
'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', 'deprecated': False},
'x11-swapped': {'id': 'X11-swapped', 'deprecated': False},
'xdebug-1.03': {'id': 'Xdebug-1.03', 'deprecated': False},
'xerox': {'id': 'Xerox', 'deprecated': False},
'xfig': {'id': 'Xfig', 'deprecated': False},
'xfree86-1.1': {'id': 'XFree86-1.1', 'deprecated': False},
'xinetd': {'id': 'xinetd', 'deprecated': False},
'xkeyboard-config-zinoviev': {'id': 'xkeyboard-config-Zinoviev', 'deprecated': False},
'xlock': {'id': 'xlock', 'deprecated': False},
'xnet': {'id': 'Xnet', 'deprecated': False},
'xpp': {'id': 'xpp', 'deprecated': False},
'xskat': {'id': 'XSkat', 'deprecated': False},
'xzoom': {'id': 'xzoom', 'deprecated': False},
'ypl-1.0': {'id': 'YPL-1.0', 'deprecated': False},
'ypl-1.1': {'id': 'YPL-1.1', 'deprecated': False},
'zed': {'id': 'Zed', 'deprecated': False},
'zeeff': {'id': 'Zeeff', 'deprecated': False},
'zend-2.0': {'id': 'Zend-2.0', 'deprecated': False},
'zimbra-1.3': {'id': 'Zimbra-1.3', 'deprecated': False},
'zimbra-1.4': {'id': 'Zimbra-1.4', 'deprecated': False},
'zlib': {'id': 'Zlib', 'deprecated': False},
'zlib-acknowledgement': {'id': 'zlib-acknowledgement', 'deprecated': False},
'zpl-1.1': {'id': 'ZPL-1.1', 'deprecated': False},
'zpl-2.0': {'id': 'ZPL-2.0', 'deprecated': False},
'zpl-2.1': {'id': 'ZPL-2.1', 'deprecated': False},
}
EXCEPTIONS: dict[str, SPDXException] = {
'389-exception': {'id': '389-exception', 'deprecated': False},
'asterisk-exception': {'id': 'Asterisk-exception', 'deprecated': False},
'asterisk-linking-protocols-exception': {'id': 'Asterisk-linking-protocols-exception', 'deprecated': False},
'autoconf-exception-2.0': {'id': 'Autoconf-exception-2.0', 'deprecated': False},
'autoconf-exception-3.0': {'id': 'Autoconf-exception-3.0', 'deprecated': False},
'autoconf-exception-generic': {'id': 'Autoconf-exception-generic', 'deprecated': False},
'autoconf-exception-generic-3.0': {'id': 'Autoconf-exception-generic-3.0', 'deprecated': False},
'autoconf-exception-macro': {'id': 'Autoconf-exception-macro', 'deprecated': False},
'bison-exception-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False},
'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False},
'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False},
'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False},
'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False},
'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False},
'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False},
'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False},
'erlang-otp-linking-exception': {'id': 'erlang-otp-linking-exception', 'deprecated': False},
'fawkes-runtime-exception': {'id': 'Fawkes-Runtime-exception', 'deprecated': False},
'fltk-exception': {'id': 'FLTK-exception', 'deprecated': False},
'fmt-exception': {'id': 'fmt-exception', 'deprecated': False},
'font-exception-2.0': {'id': 'Font-exception-2.0', 'deprecated': False},
'freertos-exception-2.0': {'id': 'freertos-exception-2.0', 'deprecated': False},
'gcc-exception-2.0': {'id': 'GCC-exception-2.0', 'deprecated': False},
'gcc-exception-2.0-note': {'id': 'GCC-exception-2.0-note', 'deprecated': False},
'gcc-exception-3.1': {'id': 'GCC-exception-3.1', 'deprecated': False},
'gmsh-exception': {'id': 'Gmsh-exception', 'deprecated': False},
'gnat-exception': {'id': 'GNAT-exception', 'deprecated': False},
'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False},
'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False},
'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False},
'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False},
'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False},
'gpl-3.0-linking-source-exception': {'id': 'GPL-3.0-linking-source-exception', 'deprecated': False},
'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False},
'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False},
'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False},
'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False},
'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False},
'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False},
'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False},
'libtool-exception': {'id': 'Libtool-exception', 'deprecated': False},
'linux-syscall-note': {'id': 'Linux-syscall-note', 'deprecated': False},
'llgpl': {'id': 'LLGPL', 'deprecated': False},
'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False},
'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False},
'mif-exception': {'id': 'mif-exception', 'deprecated': False},
'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True},
'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False},
'occt-exception-1.0': {'id': 'OCCT-exception-1.0', 'deprecated': False},
'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False},
'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False},
'pcre2-exception': {'id': 'PCRE2-exception', 'deprecated': False},
'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False},
'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False},
'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False},
'qt-lgpl-exception-1.1': {'id': 'Qt-LGPL-exception-1.1', 'deprecated': False},
'qwt-exception-1.0': {'id': 'Qwt-exception-1.0', 'deprecated': False},
'romic-exception': {'id': 'romic-exception', 'deprecated': False},
'rrdtool-floss-exception-2.0': {'id': 'RRDtool-FLOSS-exception-2.0', 'deprecated': False},
'sane-exception': {'id': 'SANE-exception', 'deprecated': False},
'shl-2.0': {'id': 'SHL-2.0', 'deprecated': False},
'shl-2.1': {'id': 'SHL-2.1', 'deprecated': False},
'stunnel-exception': {'id': 'stunnel-exception', 'deprecated': False},
'swi-exception': {'id': 'SWI-exception', 'deprecated': False},
'swift-exception': {'id': 'Swift-exception', 'deprecated': False},
'texinfo-exception': {'id': 'Texinfo-exception', 'deprecated': False},
'u-boot-exception-2.0': {'id': 'u-boot-exception-2.0', 'deprecated': False},
'ubdl-exception': {'id': 'UBDL-exception', 'deprecated': False},
'universal-foss-exception-1.0': {'id': 'Universal-FOSS-exception-1.0', 'deprecated': False},
'vsftpd-openssl-exception': {'id': 'vsftpd-openssl-exception', 'deprecated': False},
'wxwindows-exception-3.1': {'id': 'WxWindows-exception-3.1', 'deprecated': False},
'x11vnc-openssl-exception': {'id': 'x11vnc-openssl-exception', 'deprecated': False},
}
[console_scripts]
wheel=wheel.cli:main
[distutils.commands]
bdist_wheel=wheel.bdist_wheel:bdist_wheel

Sorry, the diff of this file is not supported yet

MIT License
Copyright (c) 2012 Daniel Holth <dholth@fastmail.fm> and contributors
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.
Metadata-Version: 2.3
Name: wheel
Version: 0.45.1
Summary: A built-package format for Python
Keywords: wheel,packaging
Author-email: Daniel Holth <dholth@fastmail.fm>
Maintainer-email: Alex Grönholm <alex.gronholm@nextday.fi>
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Topic :: System :: Archiving :: Packaging
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Dist: pytest >= 6.0.0 ; extra == "test"
Requires-Dist: setuptools >= 65 ; extra == "test"
Project-URL: Changelog, https://wheel.readthedocs.io/en/stable/news.html
Project-URL: Documentation, https://wheel.readthedocs.io/
Project-URL: Issue Tracker, https://github.com/pypa/wheel/issues
Project-URL: Source, https://github.com/pypa/wheel
Provides-Extra: test
wheel
=====
This is a command line tool for manipulating Python wheel files, as defined in
`PEP 427`_. It contains the following functionality:
* Convert ``.egg`` archives into ``.whl``
* Unpack wheel archives
* Repack wheel archives
* Add or remove tags in existing wheel archives
.. _PEP 427: https://www.python.org/dev/peps/pep-0427/
Historical note
---------------
This project used to contain the implementation of the setuptools_ ``bdist_wheel``
command, but as of setuptools v70.1, it no longer needs ``wheel`` installed for that to
work. Thus, you should install this **only** if you intend to use the ``wheel`` command
line tool!
.. _setuptools: https://pypi.org/project/setuptools/
Documentation
-------------
The documentation_ can be found on Read The Docs.
.. _documentation: https://wheel.readthedocs.io/
Code of Conduct
---------------
Everyone interacting in the wheel project's codebases, issue trackers, chat
rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
../../bin/wheel,sha256=pBhV19bQIgjS-r541fG3kLU6QtcyKaKdQ2RE9YIzeiU,249
wheel-0.45.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
wheel-0.45.1.dist-info/LICENSE.txt,sha256=MMI2GGeRCPPo6h0qZYx8pBe9_IkcmO8aifpP8MmChlQ,1107
wheel-0.45.1.dist-info/METADATA,sha256=mKz84H7m7jsxJyzeIcTVORiTb0NPMV39KvOIYhGgmjA,2313
wheel-0.45.1.dist-info/RECORD,,
wheel-0.45.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
wheel-0.45.1.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
wheel-0.45.1.dist-info/entry_points.txt,sha256=rTY1BbkPHhkGMm4Q3F0pIzJBzW2kMxoG1oriffvGdA0,104
wheel/__init__.py,sha256=mrxMnvdXACur_LWegbUfh5g5ysWZrd63UJn890wvGNk,59
wheel/__main__.py,sha256=NkMUnuTCGcOkgY0IBLgBCVC_BGGcWORx2K8jYGS12UE,455
wheel/__pycache__/__init__.cpython-311.pyc,,
wheel/__pycache__/__main__.cpython-311.pyc,,
wheel/__pycache__/_bdist_wheel.cpython-311.pyc,,
wheel/__pycache__/_setuptools_logging.cpython-311.pyc,,
wheel/__pycache__/bdist_wheel.cpython-311.pyc,,
wheel/__pycache__/macosx_libfile.cpython-311.pyc,,
wheel/__pycache__/metadata.cpython-311.pyc,,
wheel/__pycache__/util.cpython-311.pyc,,
wheel/__pycache__/wheelfile.cpython-311.pyc,,
wheel/_bdist_wheel.py,sha256=UghCQjSH_pVfcZh6oRjzSw_TQhcf3anSx1OkiLSL82M,21694
wheel/_setuptools_logging.py,sha256=-5KC-lne0ilOUWIDfOkqapUWGMFZhuKYDIavIZiB5kM,781
wheel/bdist_wheel.py,sha256=tpf9WufiSO1RuEMg5oPhIfSG8DMziCZ_4muCKF69Cqo,1107
wheel/cli/__init__.py,sha256=Npq6_jKi03dhIcRnmbuFhwviVJxwO0tYEnEhWMv9cJo,4402
wheel/cli/__pycache__/__init__.cpython-311.pyc,,
wheel/cli/__pycache__/convert.cpython-311.pyc,,
wheel/cli/__pycache__/pack.cpython-311.pyc,,
wheel/cli/__pycache__/tags.cpython-311.pyc,,
wheel/cli/__pycache__/unpack.cpython-311.pyc,,
wheel/cli/convert.py,sha256=Bi0ntEXb9nTllCxWeTRQ4j-nPs3szWSEKipG_GgnMkQ,12634
wheel/cli/pack.py,sha256=CAFcHdBVulvsHYJlndKVO7KMI9JqBTZz5ii0PKxxCOs,3103
wheel/cli/tags.py,sha256=lHw-LaWrkS5Jy_qWcw-6pSjeNM6yAjDnqKI3E5JTTCU,4760
wheel/cli/unpack.py,sha256=Y_J7ynxPSoFFTT7H0fMgbBlVErwyDGcObgme5MBuz58,1021
wheel/macosx_libfile.py,sha256=k1x7CE3LPtOVGqj6NXQ1nTGYVPaeRrhVzUG_KPq3zDs,16572
wheel/metadata.py,sha256=JC4p7jlQZu2bUTAQ2fevkqLjg_X6gnNyRhLn6OUO1tc,6171
wheel/util.py,sha256=aL7aibHwYUgfc8WlolL5tXdkV4DatbJxZHb1kwHFJAU,423
wheel/vendored/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
wheel/vendored/__pycache__/__init__.cpython-311.pyc,,
wheel/vendored/packaging/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
wheel/vendored/packaging/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
wheel/vendored/packaging/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
wheel/vendored/packaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
wheel/vendored/packaging/__pycache__/__init__.cpython-311.pyc,,
wheel/vendored/packaging/__pycache__/_elffile.cpython-311.pyc,,
wheel/vendored/packaging/__pycache__/_manylinux.cpython-311.pyc,,
wheel/vendored/packaging/__pycache__/_musllinux.cpython-311.pyc,,
wheel/vendored/packaging/__pycache__/_parser.cpython-311.pyc,,
wheel/vendored/packaging/__pycache__/_structures.cpython-311.pyc,,
wheel/vendored/packaging/__pycache__/_tokenizer.cpython-311.pyc,,
wheel/vendored/packaging/__pycache__/markers.cpython-311.pyc,,
wheel/vendored/packaging/__pycache__/requirements.cpython-311.pyc,,
wheel/vendored/packaging/__pycache__/specifiers.cpython-311.pyc,,
wheel/vendored/packaging/__pycache__/tags.cpython-311.pyc,,
wheel/vendored/packaging/__pycache__/utils.cpython-311.pyc,,
wheel/vendored/packaging/__pycache__/version.cpython-311.pyc,,
wheel/vendored/packaging/_elffile.py,sha256=hbmK8OD6Z7fY6hwinHEUcD1by7czkGiNYu7ShnFEk2k,3266
wheel/vendored/packaging/_manylinux.py,sha256=P7sdR5_7XBY09LVYYPhHmydMJIIwPXWsh4olk74Uuj4,9588
wheel/vendored/packaging/_musllinux.py,sha256=z1s8To2hQ0vpn_d-O2i5qxGwEK8WmGlLt3d_26V7NeY,2674
wheel/vendored/packaging/_parser.py,sha256=4tT4emSl2qTaU7VTQE1Xa9o1jMPCsBezsYBxyNMUN-s,10347
wheel/vendored/packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431
wheel/vendored/packaging/_tokenizer.py,sha256=alCtbwXhOFAmFGZ6BQ-wCTSFoRAJ2z-ysIf7__MTJ_k,5292
wheel/vendored/packaging/markers.py,sha256=_TSPI1BhJYO7Bp9AzTmHQxIqHEVXaTjmDh9G-w8qzPA,8232
wheel/vendored/packaging/requirements.py,sha256=dgoBeVprPu2YE6Q8nGfwOPTjATHbRa_ZGLyXhFEln6Q,2933
wheel/vendored/packaging/specifiers.py,sha256=IWSt0SrLSP72heWhAC8UL0eGvas7XIQHjqiViVfmPKE,39778
wheel/vendored/packaging/tags.py,sha256=fedHXiOHkBxNZTXotXv8uXPmMFU9ae-TKBujgYHigcA,18950
wheel/vendored/packaging/utils.py,sha256=XgdmP3yx9-wQEFjO7OvMj9RjEf5JlR5HFFR69v7SQ9E,5268
wheel/vendored/packaging/version.py,sha256=PFJaYZDxBgyxkfYhH3SQw4qfE9ICCWrTmitvq14y3bs,16234
wheel/vendored/vendor.txt,sha256=Z2ENjB1i5prfez8CdM1Sdr3c6Zxv2rRRolMpLmBncAE,16
wheel/wheelfile.py,sha256=USCttNlJwafxt51YYFFKG7jnxz8dfhbyqAZL6jMTA9s,8411

Sorry, the diff of this file is not supported yet

Wheel-Version: 1.0
Generator: flit 3.10.1
Root-Is-Purelib: true
Tag: py3-none-any
"""
Create a wheel (.whl) distribution.
A wheel is a built archive format.
"""
from __future__ import annotations
import os
import re
import shutil
import stat
import struct
import sys
import sysconfig
import warnings
from email.generator import BytesGenerator, Generator
from email.policy import EmailPolicy
from glob import iglob
from shutil import rmtree
from typing import TYPE_CHECKING, Callable, Iterable, Literal, Sequence, cast
from zipfile import ZIP_DEFLATED, ZIP_STORED
import setuptools
from setuptools import Command
from . import __version__ as wheel_version
from .metadata import pkginfo_to_metadata
from .util import log
from .vendored.packaging import tags
from .vendored.packaging import version as _packaging_version
from .wheelfile import WheelFile
if TYPE_CHECKING:
import types
# ensure Python logging is configured
try:
__import__("setuptools.logging")
except ImportError:
# setuptools < ??
from . import _setuptools_logging
_setuptools_logging.configure()
def safe_name(name: str) -> str:
"""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: str) -> str:
"""
Convert an arbitrary string to a standard version string
"""
try:
# normalize the version
return str(_packaging_version.Version(version))
except _packaging_version.InvalidVersion:
version = version.replace(" ", ".")
return re.sub("[^A-Za-z0-9.]+", "-", version)
setuptools_major_version = int(setuptools.__version__.split(".")[0])
PY_LIMITED_API_PATTERN = r"cp3\d"
def _is_32bit_interpreter() -> bool:
return struct.calcsize("P") == 4
def python_tag() -> str:
return f"py{sys.version_info[0]}"
def get_platform(archive_root: str | None) -> str:
"""Return our platform name 'win32', 'linux_x86_64'"""
result = sysconfig.get_platform()
if result.startswith("macosx") and archive_root is not None:
from .macosx_libfile import calculate_macosx_platform_tag
result = calculate_macosx_platform_tag(archive_root, result)
elif _is_32bit_interpreter():
if result == "linux-x86_64":
# pip pull request #3497
result = "linux-i686"
elif result == "linux-aarch64":
# packaging pull request #234
# TODO armv8l, packaging pull request #690 => this did not land
# in pip/packaging yet
result = "linux-armv7l"
return result.replace("-", "_")
def get_flag(
var: str, fallback: bool, expected: bool = True, warn: bool = True
) -> bool:
"""Use a fallback value for determining SOABI flags if the needed config
var is unset or unavailable."""
val = sysconfig.get_config_var(var)
if val is None:
if warn:
warnings.warn(
f"Config variable '{var}' is unset, Python ABI tag may be incorrect",
RuntimeWarning,
stacklevel=2,
)
return fallback
return val == expected
def get_abi_tag() -> str | None:
"""Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy2)."""
soabi: str = sysconfig.get_config_var("SOABI")
impl = tags.interpreter_name()
if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"):
d = ""
m = ""
u = ""
if get_flag("Py_DEBUG", hasattr(sys, "gettotalrefcount"), warn=(impl == "cp")):
d = "d"
if get_flag(
"WITH_PYMALLOC",
impl == "cp",
warn=(impl == "cp" and sys.version_info < (3, 8)),
) and sys.version_info < (3, 8):
m = "m"
abi = f"{impl}{tags.interpreter_version()}{d}{m}{u}"
elif soabi and impl == "cp" and soabi.startswith("cpython"):
# non-Windows
abi = "cp" + soabi.split("-")[1]
elif soabi and impl == "cp" and soabi.startswith("cp"):
# Windows
abi = soabi.split("-")[0]
elif soabi and impl == "pp":
# we want something like pypy36-pp73
abi = "-".join(soabi.split("-")[:2])
abi = abi.replace(".", "_").replace("-", "_")
elif soabi and impl == "graalpy":
abi = "-".join(soabi.split("-")[:3])
abi = abi.replace(".", "_").replace("-", "_")
elif soabi:
abi = soabi.replace(".", "_").replace("-", "_")
else:
abi = None
return abi
def safer_name(name: str) -> str:
return safe_name(name).replace("-", "_")
def safer_version(version: str) -> str:
return safe_version(version).replace("-", "_")
def remove_readonly(
func: Callable[..., object],
path: str,
excinfo: tuple[type[Exception], Exception, types.TracebackType],
) -> None:
remove_readonly_exc(func, path, excinfo[1])
def remove_readonly_exc(func: Callable[..., object], path: str, exc: Exception) -> None:
os.chmod(path, stat.S_IWRITE)
func(path)
class bdist_wheel(Command):
description = "create a wheel distribution"
supported_compressions = {
"stored": ZIP_STORED,
"deflated": ZIP_DEFLATED,
}
user_options = [
("bdist-dir=", "b", "temporary directory for creating the distribution"),
(
"plat-name=",
"p",
"platform name to embed in generated filenames "
f"(default: {get_platform(None)})",
),
(
"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]",
),
("universal", None, "make a universal wheel (default: false)"),
(
"compression=",
None,
"zipfile compression (one of: {}) (default: 'deflated')".format(
", ".join(supported_compressions)
),
),
(
"python-tag=",
None,
f"Python implementation compatibility tag (default: '{python_tag()}')",
),
(
"build-number=",
None,
"Build number for this particular version. "
"As specified in PEP-0427, this must start with a digit. "
"[default: None]",
),
(
"py-limited-api=",
None,
"Python tag (cp32|cp33|cpNN) for abi3 wheel tag (default: false)",
),
]
boolean_options = ["keep-temp", "skip-build", "relative", "universal"]
def initialize_options(self):
self.bdist_dir: str = None
self.data_dir = None
self.plat_name: str | None = None
self.plat_tag = None
self.format = "zip"
self.keep_temp = False
self.dist_dir: str | None = None
self.egginfo_dir = None
self.root_is_pure: bool | None = None
self.skip_build = None
self.relative = False
self.owner = None
self.group = None
self.universal: bool = False
self.compression: str | int = "deflated"
self.python_tag: str = python_tag()
self.build_number: str | None = None
self.py_limited_api: str | Literal[False] = False
self.plat_name_supplied = False
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, "wheel")
egg_info = self.distribution.get_command_obj("egg_info")
egg_info.ensure_finalized() # needed for correct `wheel_dist_name`
self.data_dir = self.wheel_dist_name + ".data"
self.plat_name_supplied = self.plat_name is not None
try:
self.compression = self.supported_compressions[self.compression]
except KeyError:
raise ValueError(f"Unsupported compression: {self.compression}") from None
need_options = ("dist_dir", "plat_name", "skip_build")
self.set_undefined_options("bdist", *zip(need_options, need_options))
self.root_is_pure = not (
self.distribution.has_ext_modules() or self.distribution.has_c_libraries()
)
if self.py_limited_api and not re.match(
PY_LIMITED_API_PATTERN, self.py_limited_api
):
raise ValueError(f"py-limited-api must match '{PY_LIMITED_API_PATTERN}'")
# Support legacy [wheel] section for setting universal
wheel = self.distribution.get_option_dict("wheel")
if "universal" in wheel:
# please don't define this in your global configs
log.warning(
"The [wheel] section is deprecated. Use [bdist_wheel] instead.",
)
val = wheel["universal"][1].strip()
if val.lower() in ("1", "true", "yes"):
self.universal = True
if self.build_number is not None and not self.build_number[:1].isdigit():
raise ValueError("Build tag (build-number) must start with a digit.")
@property
def wheel_dist_name(self):
"""Return distribution full name with - replaced with _"""
components = (
safer_name(self.distribution.get_name()),
safer_version(self.distribution.get_version()),
)
if self.build_number:
components += (self.build_number,)
return "-".join(components)
def get_tag(self) -> tuple[str, str, str]:
# bdist sets self.plat_name if unset, we should only use it for purepy
# wheels if the user supplied it.
if self.plat_name_supplied:
plat_name = cast(str, self.plat_name)
elif self.root_is_pure:
plat_name = "any"
else:
# macosx contains system version in platform name so need special handle
if self.plat_name and not self.plat_name.startswith("macosx"):
plat_name = self.plat_name
else:
# on macosx always limit the platform name to comply with any
# c-extension modules in bdist_dir, since the user can specify
# a higher MACOSX_DEPLOYMENT_TARGET via tools like CMake
# on other platforms, and on macosx if there are no c-extension
# modules, use the default platform name.
plat_name = get_platform(self.bdist_dir)
if _is_32bit_interpreter():
if plat_name in ("linux-x86_64", "linux_x86_64"):
plat_name = "linux_i686"
if plat_name in ("linux-aarch64", "linux_aarch64"):
# TODO armv8l, packaging pull request #690 => this did not land
# in pip/packaging yet
plat_name = "linux_armv7l"
plat_name = (
plat_name.lower().replace("-", "_").replace(".", "_").replace(" ", "_")
)
if self.root_is_pure:
if self.universal:
impl = "py2.py3"
else:
impl = self.python_tag
tag = (impl, "none", plat_name)
else:
impl_name = tags.interpreter_name()
impl_ver = tags.interpreter_version()
impl = impl_name + impl_ver
# We don't work on CPython 3.1, 3.0.
if self.py_limited_api and (impl_name + impl_ver).startswith("cp3"):
impl = self.py_limited_api
abi_tag = "abi3"
else:
abi_tag = str(get_abi_tag()).lower()
tag = (impl, abi_tag, plat_name)
# issue gh-374: allow overriding plat_name
supported_tags = [
(t.interpreter, t.abi, plat_name) for t in tags.sys_tags()
]
assert (
tag in supported_tags
), f"would build wheel with unsupported tag {tag}"
return tag
def run(self):
build_scripts = self.reinitialize_command("build_scripts")
build_scripts.executable = "python"
build_scripts.force = True
build_ext = self.reinitialize_command("build_ext")
build_ext.inplace = False
if not self.skip_build:
self.run_command("build")
install = self.reinitialize_command("install", reinit_subcommands=True)
install.root = self.bdist_dir
install.compile = False
install.skip_build = self.skip_build
install.warn_dir = False
# A wheel without setuptools scripts is more cross-platform.
# Use the (undocumented) `no_ep` option to setuptools'
# install_scripts command to avoid creating entry point scripts.
install_scripts = self.reinitialize_command("install_scripts")
install_scripts.no_ep = True
# Use a custom scheme for the archive, because we have to decide
# at installation time which scheme to use.
for key in ("headers", "scripts", "data", "purelib", "platlib"):
setattr(install, "install_" + key, os.path.join(self.data_dir, key))
basedir_observed = ""
if os.name == "nt":
# win32 barfs if any of these are ''; could be '.'?
# (distutils.command.install:change_roots bug)
basedir_observed = os.path.normpath(os.path.join(self.data_dir, ".."))
self.install_libbase = self.install_lib = basedir_observed
setattr(
install,
"install_purelib" if self.root_is_pure else "install_platlib",
basedir_observed,
)
log.info(f"installing to {self.bdist_dir}")
self.run_command("install")
impl_tag, abi_tag, plat_tag = self.get_tag()
archive_basename = f"{self.wheel_dist_name}-{impl_tag}-{abi_tag}-{plat_tag}"
if not self.relative:
archive_root = self.bdist_dir
else:
archive_root = os.path.join(
self.bdist_dir, self._ensure_relative(install.install_base)
)
self.set_undefined_options("install_egg_info", ("target", "egginfo_dir"))
distinfo_dirname = (
f"{safer_name(self.distribution.get_name())}-"
f"{safer_version(self.distribution.get_version())}.dist-info"
)
distinfo_dir = os.path.join(self.bdist_dir, distinfo_dirname)
self.egg2dist(self.egginfo_dir, distinfo_dir)
self.write_wheelfile(distinfo_dir)
# Make the archive
if not os.path.exists(self.dist_dir):
os.makedirs(self.dist_dir)
wheel_path = os.path.join(self.dist_dir, archive_basename + ".whl")
with WheelFile(wheel_path, "w", self.compression) as wf:
wf.write_files(archive_root)
# Add to 'Distribution.dist_files' so that the "upload" command works
getattr(self.distribution, "dist_files", []).append(
(
"bdist_wheel",
"{}.{}".format(*sys.version_info[:2]), # like 3.7
wheel_path,
)
)
if not self.keep_temp:
log.info(f"removing {self.bdist_dir}")
if not self.dry_run:
if sys.version_info < (3, 12):
rmtree(self.bdist_dir, onerror=remove_readonly)
else:
rmtree(self.bdist_dir, onexc=remove_readonly_exc)
def write_wheelfile(
self, wheelfile_base: str, generator: str = f"bdist_wheel ({wheel_version})"
):
from email.message import Message
msg = Message()
msg["Wheel-Version"] = "1.0" # of the spec
msg["Generator"] = generator
msg["Root-Is-Purelib"] = str(self.root_is_pure).lower()
if self.build_number is not None:
msg["Build"] = self.build_number
# Doesn't work for bdist_wininst
impl_tag, abi_tag, plat_tag = self.get_tag()
for impl in impl_tag.split("."):
for abi in abi_tag.split("."):
for plat in plat_tag.split("."):
msg["Tag"] = "-".join((impl, abi, plat))
wheelfile_path = os.path.join(wheelfile_base, "WHEEL")
log.info(f"creating {wheelfile_path}")
with open(wheelfile_path, "wb") as f:
BytesGenerator(f, maxheaderlen=0).flatten(msg)
def _ensure_relative(self, path: str) -> str:
# copied from dir_util, deleted
drive, path = os.path.splitdrive(path)
if path[0:1] == os.sep:
path = drive + path[1:]
return path
@property
def license_paths(self) -> Iterable[str]:
if setuptools_major_version >= 57:
# Setuptools has resolved any patterns to actual file names
return self.distribution.metadata.license_files or ()
files: set[str] = set()
metadata = self.distribution.get_option_dict("metadata")
if setuptools_major_version >= 42:
# Setuptools recognizes the license_files option but does not do globbing
patterns = cast(Sequence[str], self.distribution.metadata.license_files)
else:
# Prior to those, wheel is entirely responsible for handling license files
if "license_files" in metadata:
patterns = metadata["license_files"][1].split()
else:
patterns = ()
if "license_file" in metadata:
warnings.warn(
'The "license_file" option is deprecated. Use "license_files" instead.',
DeprecationWarning,
stacklevel=2,
)
files.add(metadata["license_file"][1])
if not files and not patterns and not isinstance(patterns, list):
patterns = ("LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*")
for pattern in patterns:
for path in iglob(pattern):
if path.endswith("~"):
log.debug(
f'ignoring license file "{path}" as it looks like a backup'
)
continue
if path not in files and os.path.isfile(path):
log.info(
f'adding license file "{path}" (matched pattern "{pattern}")'
)
files.add(path)
return files
def egg2dist(self, egginfo_path: str, distinfo_path: str):
"""Convert an .egg-info directory into a .dist-info directory"""
def adios(p: str) -> None:
"""Appropriately delete directory, file or link."""
if os.path.exists(p) and not os.path.islink(p) and os.path.isdir(p):
shutil.rmtree(p)
elif os.path.exists(p):
os.unlink(p)
adios(distinfo_path)
if not os.path.exists(egginfo_path):
# There is no egg-info. This is probably because the egg-info
# file/directory is not named matching the distribution name used
# to name the archive file. Check for this case and report
# accordingly.
import glob
pat = os.path.join(os.path.dirname(egginfo_path), "*.egg-info")
possible = glob.glob(pat)
err = f"Egg metadata expected at {egginfo_path} but not found"
if possible:
alt = os.path.basename(possible[0])
err += f" ({alt} found - possible misnamed archive file?)"
raise ValueError(err)
if os.path.isfile(egginfo_path):
# .egg-info is a single file
pkg_info = pkginfo_to_metadata(egginfo_path, egginfo_path)
os.mkdir(distinfo_path)
else:
# .egg-info is a directory
pkginfo_path = os.path.join(egginfo_path, "PKG-INFO")
pkg_info = pkginfo_to_metadata(egginfo_path, pkginfo_path)
# ignore common egg metadata that is useless to wheel
shutil.copytree(
egginfo_path,
distinfo_path,
ignore=lambda x, y: {
"PKG-INFO",
"requires.txt",
"SOURCES.txt",
"not-zip-safe",
},
)
# delete dependency_links if it is only whitespace
dependency_links_path = os.path.join(distinfo_path, "dependency_links.txt")
with open(dependency_links_path, encoding="utf-8") as dependency_links_file:
dependency_links = dependency_links_file.read().strip()
if not dependency_links:
adios(dependency_links_path)
pkg_info_path = os.path.join(distinfo_path, "METADATA")
serialization_policy = EmailPolicy(
utf8=True,
mangle_from_=False,
max_line_length=0,
)
with open(pkg_info_path, "w", encoding="utf-8") as out:
Generator(out, policy=serialization_policy).flatten(pkg_info)
for license_path in self.license_paths:
filename = os.path.basename(license_path)
shutil.copy(license_path, os.path.join(distinfo_path, filename))
adios(egginfo_path)
This software is made available under the terms of *either* of the licenses
found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
under the terms of *both* these licenses.

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

import subprocess
import pytest
@pytest.mark.uses_network
def test_pbr_integration(pbr_package, venv):
"""Ensure pbr packages install."""
cmd = [
'python',
'-m',
'pip',
'-v',
'install',
'--no-build-isolation',
pbr_package,
]
venv.run(cmd, stderr=subprocess.STDOUT)
out = venv.run(["python", "-c", "import mypkg.hello"])
assert "Hello world!" in out
from setuptools import _scripts
class TestWindowsScriptWriter:
def test_header(self):
hdr = _scripts.WindowsScriptWriter.get_header('')
assert hdr.startswith('#!')
assert hdr.endswith('\n')
hdr = hdr.lstrip('#!')
hdr = hdr.rstrip('\n')
# header should not start with an escaped quote
assert not hdr.startswith('\\"')
import stat
import sys
from unittest.mock import Mock
from setuptools import _shutil
def test_rmtree_readonly(monkeypatch, tmp_path):
"""Verify onerr works as expected"""
tmp_dir = tmp_path / "with_readonly"
tmp_dir.mkdir()
some_file = tmp_dir.joinpath("file.txt")
some_file.touch()
some_file.chmod(stat.S_IREAD)
expected_count = 1 if sys.platform.startswith("win") else 0
chmod_fn = Mock(wraps=_shutil.attempt_chmod_verbose)
monkeypatch.setattr(_shutil, "attempt_chmod_verbose", chmod_fn)
_shutil.rmtree(tmp_dir)
assert chmod_fn.call_count == expected_count
assert not tmp_dir.is_dir()

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display