diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1137b96..8e165f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,26 +17,28 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.15'] steps: - - uses: actions/checkout@v3 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + - uses: actions/checkout@v7.0.1 with: + persist-credentials: false + + - name: Install uv + caching + uses: astral-sh/setup-uv@v10.0.0 + with: + enable-cache: true + cache-dependency-glob: | + setup.* + tox.ini + pyproject.toml python-version: ${{ matrix.python-version }} - - name: Install depenencies (Ubuntu) + - name: Install dependencies (Ubuntu) if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y mercurial subversion - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions - - name: Test with tox - run: tox + run: uvx --with tox-uv --with tox-gh-actions tox diff --git a/CHANGES.rst b/CHANGES.rst index c732cc7..6976bcd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,10 +2,12 @@ Changelog ========= -2.1.1 (unreleased) +3.0.0 (unreleased) ------------------ -- Nothing changed yet. +- Add support for Python 3.14 and 3.15. [maurits] + +- Require Python 3.10 or higher. [maurits] 2.1.0 (2026-08-20) diff --git a/MANIFEST.in b/MANIFEST.in index 4e18b6b..c80f692 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,2 @@ include *.rst include *.ini -prune mr.developer.addon diff --git a/setup.cfg b/setup.cfg index 1b713b6..a49c593 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal = 1 - [zest.releaser] version-levels = 2 diff --git a/setup.py b/setup.py index 618b344..cf4be76 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,12 @@ from setuptools import setup -version = '2.1.1.dev0' +version = '3.0.0.dev0' install_requires = [ 'setuptools', 'zc.buildout', - 'six', ] tests_require = [ @@ -33,25 +32,19 @@ def get_text_from_file(fn): classifiers=[ "Development Status :: 5 - Production/Stable", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Framework :: Buildout", "Topic :: Software Development :: Libraries :: Python Modules"], keywords='buildout extension vcs git develop', author='Florian Schulze', author_email='florian.schulze@gmx.net', - url='http://github.com/fschulze/mr.developer', + url='https://github.com/fschulze/mr.developer', license='BSD', packages=['mr', 'mr.developer', 'mr.developer.tests'], package_dir={'': 'src'}, @@ -61,7 +54,7 @@ def get_text_from_file(fn): install_requires=install_requires, tests_require=tests_require, extras_require=extras_require, - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", + python_requires=">=3.10", test_suite='mr.developer.tests', entry_points=""" [console_scripts] diff --git a/src/mr/developer/bazaar.py b/src/mr/developer/bazaar.py index b0a8542..a3af456 100644 --- a/src/mr/developer/bazaar.py +++ b/src/mr/developer/bazaar.py @@ -12,7 +12,7 @@ class BazaarError(common.WCError): class BazaarWorkingCopy(common.BaseWorkingCopy): def __init__(self, source): - super(BazaarWorkingCopy, self).__init__(source) + super().__init__(source) self.bzr_executable = common.which('bzr') def bzr_branch(self, **kwargs): @@ -32,7 +32,7 @@ def bzr_branch(self, **kwargs): stdout, stderr = cmd.communicate() if cmd.returncode != 0: raise BazaarError( - 'bzr branch for %r failed.\n%s' % (name, stderr)) + f'bzr branch for {name!r} failed.\n{stderr}') if kwargs.get('verbose', False): return stdout @@ -49,7 +49,7 @@ def bzr_pull(self, **kwargs): stdout, stderr = cmd.communicate() if cmd.returncode != 0: raise BazaarError( - 'bzr pull for %r failed.\n%s' % (name, stderr)) + f'bzr pull for {name!r} failed.\n{stderr}') if kwargs.get('verbose', False): return stdout @@ -81,7 +81,7 @@ def matches(self): stdout, stderr = cmd.communicate() if cmd.returncode != 0: raise BazaarError( - 'bzr info for %r failed.\n%s' % (name, stderr)) + f'bzr info for {name!r} failed.\n{stderr}') return (self.source['url'] in stdout.split()) def status(self, **kwargs): diff --git a/src/mr/developer/commands.py b/src/mr/developer/commands.py index ec70db3..4b49448 100644 --- a/src/mr/developer/commands.py +++ b/src/mr/developer/commands.py @@ -1,11 +1,9 @@ -from __future__ import print_function from mr.developer.common import logger, memoize, WorkingCopies, yesno import argparse import errno import os import re import shutil -import six import stat import subprocess import sys @@ -15,7 +13,7 @@ class ChoicesPseudoAction(argparse.Action): def __init__(self, *args, **kwargs): - sup = super(ChoicesPseudoAction, self) + sup = super() sup.__init__(dest=args[0], option_strings=list(args), help=kwargs.get('help'), nargs=0) @@ -36,11 +34,11 @@ def _fill_text(self, text, width, indent): result = [] for line in text.split("\n"): for line2 in textwrap.fill(line, width).split("\n"): - result.append("%s%s" % (indent, line2)) + result.append(f"{indent}{line2}") return "\n".join(result) -class Command(object): +class Command: def __init__(self, develop): self.develop = develop @@ -71,7 +69,7 @@ def get_packages(self, args, auto_checkout=False, if len(result) == 0: if len(args) > 1: - regexps = "%s or '%s'" % (", ".join("'%s'" % x for x in args[:-1]), args[-1]) + regexps = "{} or '{}'".format(", ".join("'%s'" % x for x in args[:-1]), args[-1]) else: regexps = "'%s'" % args[0] logger.error("No package matched %s." % regexps) @@ -302,7 +300,7 @@ def __call__(self, args): for name in sorted(cmds): cmd = cmds[name] if len(cmd['aliases']): - header = "%s (%s)" % (name, ", ".join(cmd['aliases'])) + header = "{} ({})".format(name, ", ".join(cmd['aliases'])) else: header = name print(header) @@ -319,7 +317,7 @@ def __call__(self, args): for name in sorted(cmds): cmd = cmds[name] if len(cmd['aliases']): - print(" %s (%s)" % (name, ", ".join(cmd['aliases']))) + print(" {} ({})".format(name, ", ".join(cmd['aliases']))) else: print(" %s" % name) @@ -464,7 +462,7 @@ def __call__(self, args): else: info.append("#") if args.long: - info.append("(%s) %s %s" % (source['kind'], name, source['url'])) + info.append("({}) {} {}".format(source['kind'], name, source['url'])) else: info.append(name) print(" ".join(info)) @@ -560,7 +558,7 @@ def __call__(self, args): need_force = False if source['kind'] != 'svn': need_force = True - logger.warn("The directory of package '%s' at '%s' might contain unrecoverable files and will not be removed without --force." % (name, path)) + logger.warn(f"The directory of package '{name}' at '{path}' might contain unrecoverable files and will not be removed without --force.") if workingcopies.status(source) != 'clean': need_force = True logger.warn("The package '%s' is dirty and will not be removed without --force." % name) @@ -578,7 +576,7 @@ def __call__(self, args): if answer == 'all': force_all = True - logger.info("Removing package '%s' at '%s'." % (name, path)) + logger.info(f"Removing package '{name}' at '{path}'.") if not args.dry_run: shutil.rmtree(source['path'], ignore_errors=False, @@ -756,7 +754,7 @@ def __call__(self, args): info.append(name) print(" ".join(info)) if args.verbose: - if six.PY3 and isinstance(output, six.binary_type): + if isinstance(output, bytes): output = output.decode('utf8') output = output.strip() if output: diff --git a/src/mr/developer/common.py b/src/mr/developer/common.py index 08d7d49..4e71ef1 100644 --- a/src/mr/developer/common.py +++ b/src/mr/developer/common.py @@ -2,19 +2,11 @@ import os import pkg_resources import platform -try: - import queue -except ImportError: - import Queue as queue +import queue import re -import subprocess -import six import sys import threading -if sys.version_info < (3, ): - from ConfigParser import RawConfigParser -else: - from configparser import RawConfigParser +from configparser import RawConfigParser logger = logging.getLogger("mr.developer") @@ -26,18 +18,6 @@ def print_stderr(s): sys.stderr.flush() -try: - advance_iterator = next -except NameError: - def advance_iterator(it): - return it.next() - -try: - raw_input = raw_input -except NameError: - raw_input = input - - # shameless copy from # http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python def which(name_root, default=None): @@ -105,7 +85,7 @@ class WCError(Exception): """ A working copy error. """ -class BaseWorkingCopy(object): +class BaseWorkingCopy: def __init__(self, source): self._output = [] self.output = self._output.append @@ -145,7 +125,7 @@ def yesno(question, default=True, all=True): else: question = "%s] " % question while 1: - answer = raw_input(question).lower() + answer = input(question).lower() for option in answers: if answer in answers[option]: return option @@ -193,7 +173,7 @@ def worker(working_copies, the_queue): lvl(msg) if kwargs.get('verbose', False) and output is not None and output.strip(): - if six.PY3 and isinstance(output, six.binary_type): + if isinstance(output, bytes): output = output.decode('utf8') print(output) output_lock.release() @@ -244,7 +224,7 @@ def get_commands(): return commands.values() -class WorkingCopies(object): +class WorkingCopies: def __init__(self, sources, threads=5): self.sources = sources self.threads = threads @@ -255,15 +235,6 @@ def process(self, the_queue): if self.threads < 2: worker(self, the_queue) else: - if sys.version_info < (2, 6): - # work around a race condition in subprocess - _old_subprocess_cleanup = subprocess._cleanup - - def _cleanup(): - pass - - subprocess._cleanup = _cleanup - threads = [] for i in range(self.threads): @@ -272,9 +243,6 @@ def _cleanup(): threads.append(thread) for thread in threads: thread.join() - if sys.version_info < (2, 6): - subprocess._cleanup = _old_subprocess_cleanup - subprocess._cleanup() if self.errors: logger.error("There have been errors, see messages above.") @@ -479,7 +447,7 @@ def parse_buildout_args(args): return options, settings, args -class Rewrite(object): +class Rewrite: _matcher = re.compile(r"(?P