setuptools
Advanced tools
| 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
| importlib_resources |
| 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
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&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&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&' | ||
| 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 |
@@ -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). |
+4
-4
@@ -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 @@ |
+19
-22
| [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" |
+2
-3
@@ -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 |
+1
-1
@@ -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 @@ |
+1
-1
@@ -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 |
+41
-156
@@ -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 |
+8
-15
@@ -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 @@ |
+42
-31
@@ -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. |
+102
-217
| 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. |
+15
-35
@@ -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'**' |
+28
-37
@@ -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[ |
+69
-90
@@ -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 * '*'}""" |
+32
-57
@@ -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 @@ |
+5
-2
@@ -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
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
5205729
2.11%622
6.87%89503
4.55%