diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86fd5f7..6f49f73 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,6 +43,9 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install setuptools + run: pip install "setuptools<72" + - name: Build module run: python setup.py build @@ -68,6 +71,9 @@ jobs: - "3.9" - "3.10" - "3.11" + - "3.12" + - "3.13" + - "3.14" steps: - uses: actions/checkout@v4 @@ -77,6 +83,9 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Install setuptools + run: pip install "setuptools<72" + - name: Build module run: python setup.py build diff --git a/appveyor.yml b/appveyor.yml index f164a19..2830e53 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,6 +7,12 @@ environment: - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python37" + - PYTHON: "C:\\Python312" + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 + INSTALL_SETUPTOOLS: "1" + - PYTHON: "C:\\Python313" + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 + INSTALL_SETUPTOOLS: "1" - PYTHON: "C:\\Python26-x64" - PYTHON: "C:\\Python27-x64" - PYTHON: "C:\\Python33-x64" @@ -16,6 +22,12 @@ environment: - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python37-x64" + - PYTHON: "C:\\Python312-x64" + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 + INSTALL_SETUPTOOLS: "1" + - PYTHON: "C:\\Python313-x64" + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 + INSTALL_SETUPTOOLS: "1" build: off diff --git a/build.cmd b/build.cmd index 23df2b6..ef033f7 100644 --- a/build.cmd +++ b/build.cmd @@ -18,4 +18,9 @@ IF "%DISTUTILS_USE_SDK%"=="1" ( ECHO Using default MSVC build environment ) +IF "%INSTALL_SETUPTOOLS%"=="1" ( + ECHO Installing setuptools + %PYTHON%\\python.exe -m pip install "setuptools<72" +) + CALL %* diff --git a/ez_setup.py b/ez_setup.py deleted file mode 100644 index 800c31e..0000000 --- a/ez_setup.py +++ /dev/null @@ -1,414 +0,0 @@ -#!/usr/bin/env python - -""" -Setuptools bootstrapping installer. - -Maintained at https://github.com/pypa/setuptools/tree/bootstrap. - -Run this script to install or upgrade setuptools. - -This method is DEPRECATED. Check https://github.com/pypa/setuptools/issues/581 for more details. -""" - -import os -import shutil -import sys -import tempfile -import zipfile -import optparse -import subprocess -import platform -import textwrap -import contextlib - -from distutils import log - -try: - from urllib.request import urlopen -except ImportError: - from urllib2 import urlopen - -try: - from site import USER_SITE -except ImportError: - USER_SITE = None - -# 33.1.1 is the last version that supports setuptools self upgrade/installation. -DEFAULT_VERSION = "33.1.1" -DEFAULT_URL = "https://pypi.io/packages/source/s/setuptools/" -DEFAULT_SAVE_DIR = os.curdir -DEFAULT_DEPRECATION_MESSAGE = "ez_setup.py is deprecated and when using it setuptools will be pinned to {0} since it's the last version that supports setuptools self upgrade/installation, check https://github.com/pypa/setuptools/issues/581 for more info; use pip to install setuptools" - -MEANINGFUL_INVALID_ZIP_ERR_MSG = 'Maybe {0} is corrupted, delete it and try again.' - -log.warn(DEFAULT_DEPRECATION_MESSAGE.format(DEFAULT_VERSION)) - - -def _python_cmd(*args): - """ - Execute a command. - - Return True if the command succeeded. - """ - args = (sys.executable,) + args - return subprocess.call(args) == 0 - - -def _install(archive_filename, install_args=()): - """Install Setuptools.""" - with archive_context(archive_filename): - # installing - log.warn('Installing Setuptools') - if not _python_cmd('setup.py', 'install', *install_args): - log.warn('Something went wrong during the installation.') - log.warn('See the error message above.') - # exitcode will be 2 - return 2 - - -def _build_egg(egg, archive_filename, to_dir): - """Build Setuptools egg.""" - with archive_context(archive_filename): - # building an egg - log.warn('Building a Setuptools egg in %s', to_dir) - _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) - # returning the result - log.warn(egg) - if not os.path.exists(egg): - raise IOError('Could not build the egg.') - - -class ContextualZipFile(zipfile.ZipFile): - - """Supplement ZipFile class to support context manager for Python 2.6.""" - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.close() - - def __new__(cls, *args, **kwargs): - """Construct a ZipFile or ContextualZipFile as appropriate.""" - if hasattr(zipfile.ZipFile, '__exit__'): - return zipfile.ZipFile(*args, **kwargs) - return super(ContextualZipFile, cls).__new__(cls) - - -@contextlib.contextmanager -def archive_context(filename): - """ - Unzip filename to a temporary directory, set to the cwd. - - The unzipped target is cleaned up after. - """ - tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) - old_wd = os.getcwd() - try: - os.chdir(tmpdir) - try: - with ContextualZipFile(filename) as archive: - archive.extractall() - except zipfile.BadZipfile as err: - if not err.args: - err.args = ('', ) - err.args = err.args + ( - MEANINGFUL_INVALID_ZIP_ERR_MSG.format(filename), - ) - raise - - # going in the directory - subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) - os.chdir(subdir) - log.warn('Now working in %s', subdir) - yield - - finally: - os.chdir(old_wd) - shutil.rmtree(tmpdir) - - -def _do_download(version, download_base, to_dir, download_delay): - """Download Setuptools.""" - py_desig = 'py{sys.version_info[0]}.{sys.version_info[1]}'.format(sys=sys) - tp = 'setuptools-{version}-{py_desig}.egg' - egg = os.path.join(to_dir, tp.format(**locals())) - if not os.path.exists(egg): - archive = download_setuptools(version, download_base, - to_dir, download_delay) - _build_egg(egg, archive, to_dir) - sys.path.insert(0, egg) - - # Remove previously-imported pkg_resources if present (see - # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). - if 'pkg_resources' in sys.modules: - _unload_pkg_resources() - - import setuptools - setuptools.bootstrap_install_from = egg - - -def use_setuptools( - version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=DEFAULT_SAVE_DIR, download_delay=15): - """ - Ensure that a setuptools version is installed. - - Return None. Raise SystemExit if the requested version - or later cannot be installed. - """ - to_dir = os.path.abspath(to_dir) - - # prior to importing, capture the module state for - # representative modules. - rep_modules = 'pkg_resources', 'setuptools' - imported = set(sys.modules).intersection(rep_modules) - - try: - import pkg_resources - pkg_resources.require("setuptools>=" + version) - # a suitable version is already installed - return - except ImportError: - # pkg_resources not available; setuptools is not installed; download - pass - except pkg_resources.DistributionNotFound: - # no version of setuptools was found; allow download - pass - except pkg_resources.VersionConflict as VC_err: - if imported: - _conflict_bail(VC_err, version) - - # otherwise, unload pkg_resources to allow the downloaded version to - # take precedence. - del pkg_resources - _unload_pkg_resources() - - return _do_download(version, download_base, to_dir, download_delay) - - -def _conflict_bail(VC_err, version): - """ - Setuptools was imported prior to invocation, so it is - unsafe to unload it. Bail out. - """ - conflict_tmpl = textwrap.dedent(""" - The required version of setuptools (>={version}) is not available, - and can't be installed while this script is running. Please - install a more recent version first, using - 'easy_install -U setuptools'. - - (Currently using {VC_err.args[0]!r}) - """) - msg = conflict_tmpl.format(**locals()) - sys.stderr.write(msg) - sys.exit(2) - - -def _unload_pkg_resources(): - sys.meta_path = [ - importer - for importer in sys.meta_path - if importer.__class__.__module__ != 'pkg_resources.extern' - ] - del_modules = [ - name for name in sys.modules - if name.startswith('pkg_resources') - ] - for mod_name in del_modules: - del sys.modules[mod_name] - - -def _clean_check(cmd, target): - """ - Run the command to download target. - - If the command fails, clean up before re-raising the error. - """ - try: - subprocess.check_call(cmd) - except subprocess.CalledProcessError: - if os.access(target, os.F_OK): - os.unlink(target) - raise - - -def download_file_powershell(url, target): - """ - Download the file at url to target using Powershell. - - Powershell will validate trust. - Raise an exception if the command cannot complete. - """ - target = os.path.abspath(target) - ps_cmd = ( - "[System.Net.WebRequest]::DefaultWebProxy.Credentials = " - "[System.Net.CredentialCache]::DefaultCredentials; " - '(new-object System.Net.WebClient).DownloadFile("%(url)s", "%(target)s")' - % locals() - ) - cmd = [ - 'powershell', - '-Command', - ps_cmd, - ] - _clean_check(cmd, target) - - -def has_powershell(): - """Determine if Powershell is available.""" - if platform.system() != 'Windows': - return False - cmd = ['powershell', '-Command', 'echo test'] - with open(os.path.devnull, 'wb') as devnull: - try: - subprocess.check_call(cmd, stdout=devnull, stderr=devnull) - except Exception: - return False - return True -download_file_powershell.viable = has_powershell - - -def download_file_curl(url, target): - cmd = ['curl', url, '--location', '--silent', '--output', target] - _clean_check(cmd, target) - - -def has_curl(): - cmd = ['curl', '--version'] - with open(os.path.devnull, 'wb') as devnull: - try: - subprocess.check_call(cmd, stdout=devnull, stderr=devnull) - except Exception: - return False - return True -download_file_curl.viable = has_curl - - -def download_file_wget(url, target): - cmd = ['wget', url, '--quiet', '--output-document', target] - _clean_check(cmd, target) - - -def has_wget(): - cmd = ['wget', '--version'] - with open(os.path.devnull, 'wb') as devnull: - try: - subprocess.check_call(cmd, stdout=devnull, stderr=devnull) - except Exception: - return False - return True -download_file_wget.viable = has_wget - - -def download_file_insecure(url, target): - """Use Python to download the file, without connection authentication.""" - src = urlopen(url) - try: - # Read all the data in one block. - data = src.read() - finally: - src.close() - - # Write all the data in one block to avoid creating a partial file. - with open(target, "wb") as dst: - dst.write(data) -download_file_insecure.viable = lambda: True - - -def get_best_downloader(): - downloaders = ( - download_file_powershell, - download_file_curl, - download_file_wget, - download_file_insecure, - ) - viable_downloaders = (dl for dl in downloaders if dl.viable()) - return next(viable_downloaders, None) - - -def download_setuptools( - version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=DEFAULT_SAVE_DIR, delay=15, - downloader_factory=get_best_downloader): - """ - Download setuptools from a specified location and return its filename. - - `version` should be a valid setuptools version number that is available - as an sdist for download under the `download_base` URL (which should end - with a '/'). `to_dir` is the directory where the egg will be downloaded. - `delay` is the number of seconds to pause before an actual download - attempt. - - ``downloader_factory`` should be a function taking no arguments and - returning a function for downloading a URL to a target. - """ - # making sure we use the absolute path - to_dir = os.path.abspath(to_dir) - zip_name = "setuptools-%s.zip" % version - url = download_base + zip_name - saveto = os.path.join(to_dir, zip_name) - if not os.path.exists(saveto): # Avoid repeated downloads - log.warn("Downloading %s", url) - downloader = downloader_factory() - downloader(url, saveto) - return os.path.realpath(saveto) - - -def _build_install_args(options): - """ - Build the arguments to 'python setup.py install' on the setuptools package. - - Returns list of command line arguments. - """ - return ['--user'] if options.user_install else [] - - -def _parse_args(): - """Parse the command line for options.""" - parser = optparse.OptionParser() - parser.add_option( - '--user', dest='user_install', action='store_true', default=False, - help='install in user site package') - parser.add_option( - '--download-base', dest='download_base', metavar="URL", - default=DEFAULT_URL, - help='alternative URL from where to download the setuptools package') - parser.add_option( - '--insecure', dest='downloader_factory', action='store_const', - const=lambda: download_file_insecure, default=get_best_downloader, - help='Use internal, non-validating downloader' - ) - parser.add_option( - '--version', help="Specify which version to download", - default=DEFAULT_VERSION, - ) - parser.add_option( - '--to-dir', - help="Directory to save (and re-use) package", - default=DEFAULT_SAVE_DIR, - ) - options, args = parser.parse_args() - # positional arguments are ignored - return options - - -def _download_args(options): - """Return args for download_setuptools function from cmdline args.""" - return dict( - version=options.version, - download_base=options.download_base, - downloader_factory=options.downloader_factory, - to_dir=options.to_dir, - ) - - -def main(): - """Install or upgrade setuptools and EasyInstall.""" - options = _parse_args() - archive = download_setuptools(**_download_args(options)) - return _install(archive, _build_install_args(options)) - -if __name__ == '__main__': - sys.exit(main()) diff --git a/setup.py b/setup.py index 01014d0..10f898d 100644 --- a/setup.py +++ b/setup.py @@ -24,17 +24,19 @@ # import sys, os from warnings import warn -from distutils import log -from distutils.command.build_ext import build_ext as _build_ext -from version import get_git_version +try: + from distutils import log +except ImportError: + from setuptools._distutils import log try: - from setuptools import setup, Extension + from distutils.command.build_ext import build_ext as _build_ext except ImportError: - from ez_setup import use_setuptools - use_setuptools() + from setuptools.command.build_ext import build_ext as _build_ext + +from version import get_git_version - from setuptools import setup, Extension +from setuptools import setup, Extension class UnsupportedPlatformWarning(Warning): pass @@ -91,7 +93,12 @@ def __new__(cls, s): if IS_WINDOWS: # don't try to import MSVC compiler on non-windows platforms # as this triggers unnecessary warnings - from distutils.msvccompiler import MSVCCompiler + try: + from distutils.msvccompiler import MSVCCompiler + except ImportError: + class MSVCCompiler(object): + # dummy marker class + pass else: class MSVCCompiler(object): # dummy marker class @@ -119,7 +126,7 @@ def build_extension(self, ext): else: ext.define_macros.append(('_7ZIP_ST', 1)) - if isinstance(self.compiler, MSVCCompiler): + if isinstance(self.compiler, MSVCCompiler) or getattr(self.compiler, 'compiler_type', '') == 'msvc': # set flags only available when using MSVC ext.extra_link_args.append('/MANIFEST') if COMPILE_DEBUG: diff --git a/src/pylzma/pylzma.c b/src/pylzma/pylzma.c index a3acae2..4229fde 100644 --- a/src/pylzma/pylzma.c +++ b/src/pylzma/pylzma.c @@ -9,16 +9,16 @@ * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. - * + * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. - * + * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * + * * $Id$ * */ @@ -75,19 +75,19 @@ pylzma_calculate_key(PyObject *self, PyObject *args, PyObject *kwargs) if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s#i|Os", kwlist, &password, &pwlen, &cycles, &pysalt, &digest)) return NULL; - + if (pysalt == Py_None) { pysalt = NULL; } else if (!PyBytes_Check(pysalt)) { PyErr_Format(PyExc_TypeError, "salt must be a string, got a %s", pysalt->ob_type->tp_name); return NULL; } - + if (strcmp(digest, "sha256") != 0) { PyErr_Format(PyExc_TypeError, "digest %s is unsupported", digest); return NULL; } - + if (pysalt != NULL) { salt = PyBytes_AS_STRING(pysalt); saltlen = PyBytes_Size(pysalt); @@ -95,7 +95,7 @@ pylzma_calculate_key(PyObject *self, PyObject *args, PyObject *kwargs) salt = NULL; saltlen = 0; } - + if (cycles == 0x3f) { int pos; int i; @@ -124,7 +124,7 @@ pylzma_calculate_key(PyObject *self, PyObject *args, PyObject *kwargs) Sha256_Final(&sha, (Byte *) &key); Py_END_ALLOW_THREADS } - + return PyBytes_FromStringAndSize(key, 32); } @@ -139,15 +139,15 @@ pylzma_bcj_x86_convert(PyObject *self, PyObject *args) PARSE_LENGTH_TYPE length; int encoding=0; PyObject *result; - + if (!PyArg_ParseTuple(args, "s#|i", &data, &length, &encoding)) { return NULL; } - + if (!length) { return PyBytes_FromString(""); } - + result = PyBytes_FromStringAndSize(data, length); if (result != NULL) { UInt32 state; @@ -156,7 +156,7 @@ pylzma_bcj_x86_convert(PyObject *self, PyObject *args) x86_Convert((Byte *) PyBytes_AS_STRING(result), length, 0, &state, encoding); Py_END_ALLOW_THREADS } - + return result; } @@ -564,7 +564,7 @@ initpylzma(void) #if 0 Py_INCREF(&CCompressionObject_Type); PyModule_AddObject(m, "compressobj", (PyObject *)&CCompressionObject_Type); -#endif +#endif Py_INCREF(&CCompressionFileObject_Type); PyModule_AddObject(m, "compressfile", (PyObject *)&CCompressionFileObject_Type); @@ -589,7 +589,9 @@ initpylzma(void) pylzma_init_compfile(); #if defined(WITH_THREAD) +#if PY_VERSION_HEX < 0x03090000 PyEval_InitThreads(); +#endif #if !defined(PYLZMA_USE_GILSTATE) /* Save the current interpreter, so compressing file objects works. */ diff --git a/tests/test_7zfiles.py b/tests/test_7zfiles.py index 2c1e99b..cb49dc2 100644 --- a/tests/test_7zfiles.py +++ b/tests/test_7zfiles.py @@ -48,7 +48,7 @@ def sorted(l): if sys.version_info[:2] < (3, 0): def bytes(s, encoding): return s - + def unicode_string(s): return s.decode('latin-1') else: @@ -122,7 +122,7 @@ def _test_umlaut_archive(self, filename): self.assertEqual(cf.read(), bytes('This file contains a german umlaut in the filename.', 'ascii')) cf.reset() self.assertEqual(cf.read(), bytes('This file contains a german umlaut in the filename.', 'ascii')) - + def test_non_solid_umlaut(self): # test loading of a non-solid archive containing files with umlauts self._test_umlaut_archive('umlaut-non_solid.7z') @@ -350,7 +350,7 @@ def suite(): ] for tc in test_cases: - suite.addTest(unittest.makeSuite(tc)) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(tc)) return suite diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index f3e3031..ca4303c 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -51,12 +51,12 @@ def generate_random(size, choice=random.choice, ALL_CHARS=ALL_CHARS): return s class TestPyLZMACompability(unittest.TestCase): - + def setUp(self): self.plain = bytes('hello, this is a test string', 'ascii') self.plain_with_eos = unhexlify('5d0000800000341949ee8def8c6b64909b1386e370bebeb1b656f5736d653c127731a214ff7031c000') self.plain_without_eos = unhexlify('5d0000800000341949ee8def8c6b64909b1386e370bebeb1b656f5736d653c115edbe9') - + def test_decompression_noeos(self): # test decompression without the end of stream marker decompressed = pylzma.decompress_compat(self.plain_without_eos) @@ -83,7 +83,7 @@ def suite(): ] for tc in test_cases: - suite.addTest(unittest.makeSuite(tc)) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(tc)) return suite diff --git a/tests/test_pylzma.py b/tests/test_pylzma.py index 629b0d3..55c7925 100644 --- a/tests/test_pylzma.py +++ b/tests/test_pylzma.py @@ -51,7 +51,7 @@ def generate_random(size, choice=random.choice, ALL_CHARS=ALL_CHARS): return s class TestPyLZMA(unittest.TestCase): - + def setUp(self): self.plain = bytes('hello, this is a test string', 'ascii') self.plain_with_eos = unhexlify('5d0000800000341949ee8def8c6b64909b1386e370bebeb1b656f5736d653c127731a214ff7031c000') @@ -64,17 +64,17 @@ def test_compression_eos(self): # test compression with end of stream marker compressed = pylzma.compress(self.plain, eos=1) self.assertEqual(compressed, self.plain_with_eos) - + def test_compression_no_eos(self): # test compression without end of stream marker compressed = pylzma.compress(self.plain, eos=0) self.assertEqual(compressed, self.plain_without_eos) - + def test_decompression_eos(self): # test decompression with the end of stream marker decompressed = pylzma.decompress(self.plain_with_eos) self.assertEqual(decompressed, self.plain) - + def test_decompression_noeos(self): # test decompression without the end of stream marker decompressed = pylzma.decompress(self.plain_without_eos, maxlength=28) @@ -109,7 +109,7 @@ def test_decompression_stream(self): data = decompress.decompress(self.plain_with_eos) data += decompress.flush() self.assertEqual(data, self.plain) - + def test_decompression_stream_two(self): # test decompression in two steps decompress = pylzma.decompressobj() @@ -187,7 +187,7 @@ def test_compression_file(self): self.assertEqual(check, self.plain) if sys.version_info[:2] < (3, 0): - + def test_compression_file_python(self): # test compressing from file-like object (Python class) from StringIO import StringIO as PyStringIO @@ -265,7 +265,7 @@ def suite(): ] for tc in test_cases: - suite.addTest(unittest.makeSuite(tc)) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(tc)) return suite diff --git a/version.py b/version.py index 141f20e..e637ec5 100644 --- a/version.py +++ b/version.py @@ -39,6 +39,12 @@ import warnings warnings.warn("Can't import subprocess module, git version will not be available.") +try: + bytes +except NameError: + # Python 3 + class bytes(object): + pass def call_git_describe(abbrev=4): try: @@ -47,8 +53,14 @@ def call_git_describe(abbrev=4): p.stderr.close() line = p.stdout.readlines()[0] version = line.strip() + if isinstance(version, bytes): + version = version.decode('utf-8') if version[:1] == 'v': version = version[1:] + pos = version.find('-g') + if pos != -1: + version = version[:pos] + version = version.replace('-', '.post') return version except: