diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0d18a51..30bb649 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,29 +17,30 @@ jobs: fail-fast: false matrix: os: - - ["ubuntu", "ubuntu-20.04"] + - ["ubuntu", "ubuntu-latest"] config: # [Python version, tox env] - - ["3.9", "lint"] - - ["3.7", "py37"] + - ["3.11", "release-check"] + - ["3.11", "lint"] - ["3.8", "py38"] - ["3.9", "py39"] - ["3.10", "py310"] - ["3.11", "py311"] - - ["3.9", "docs"] - - ["3.9", "coverage"] + - ["3.12", "py312"] + - ["3.11", "docs"] + - ["3.11", "coverage"] runs-on: ${{ matrix.os[1] }} if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name name: ${{ matrix.config[1] }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.config[0] }} - name: Pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ matrix.config[0] }}-${{ hashFiles('setup.*', 'tox.ini') }} @@ -51,7 +52,11 @@ jobs: python -m pip install --upgrade pip pip install tox - name: Test + if: ${{ !startsWith(runner.os, 'Mac') }} run: tox -e ${{ matrix.config[1] }} + - name: Test (macOS) + if: ${{ startsWith(runner.os, 'Mac') }} + run: tox -e ${{ matrix.config[1] }}-universal2 - name: Coverage if: matrix.config[1] == 'coverage' run: | diff --git a/.meta.toml b/.meta.toml index d1e7c8f..20529b4 100644 --- a/.meta.toml +++ b/.meta.toml @@ -2,7 +2,7 @@ # https://github.com/zopefoundation/meta/tree/master/config/pure-python [meta] template = "pure-python" -commit-id = "b21fbbf2" +commit-id = "994c74d7" [python] with-windows = false diff --git a/CHANGES.rst b/CHANGES.rst index 7e21984..50c943b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,10 @@ Changelog 5.2 (unreleased) ---------------- +- Add support for Python 3.12. + +- Drop support for Python 3.7. + - Add basque (eu) translation. - Fix tests to run with ``lxml >= 5.3``, thus requiring at least that version. diff --git a/setup.cfg b/setup.cfg index 09d8284..770db3c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,5 @@ # Generated from: # https://github.com/zopefoundation/meta/tree/master/config/pure-python -[bdist_wheel] -universal = 0 [flake8] doctests = 1 diff --git a/setup.py b/setup.py index f950ae2..0eea88e 100644 --- a/setup.py +++ b/setup.py @@ -48,11 +48,11 @@ def read(*rnames): 'License :: OSI Approved :: Zope Public License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - '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 :: Implementation :: CPython', 'Natural Language :: English', 'Operating System :: OS Independent', @@ -64,7 +64,7 @@ def read(*rnames): include_package_data=True, package_dir={'': 'src'}, namespace_packages=['z3c'], - python_requires='>=3.7', + python_requires='>=3.8', extras_require=dict( extra=[ 'z3c.pt >= 2.1', diff --git a/src/z3c/form/action.py b/src/z3c/form/action.py index cd1d6a8..68311e8 100644 --- a/src/z3c/form/action.py +++ b/src/z3c/form/action.py @@ -30,7 +30,7 @@ def __init__(self, action): self.action = action def __repr__(self): - return '<{} for {!r}>'.format(self.__class__.__name__, self.action) + return f'<{self.__class__.__name__} for {self.action!r}>' @zope.interface.implementer(interfaces.IActionErrorEvent) @@ -104,7 +104,7 @@ def execute(self): return result def __repr__(self): - return '<{} {!r}>'.format(self.__class__.__name__, self.__name__) + return f'<{self.__class__.__name__} {self.__name__!r}>' @zope.interface.implementer(interfaces.IActionHandler) diff --git a/src/z3c/form/browser/tests.py b/src/z3c/form/browser/tests.py index faa224d..073b5e6 100644 --- a/src/z3c/form/browser/tests.py +++ b/src/z3c/form/browser/tests.py @@ -11,11 +11,6 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -""" -$Id$ -""" -__docformat__ = "reStructuredText" - import doctest import itertools import unittest @@ -28,7 +23,7 @@ try: import z3c.pt import z3c.ptcompat # noqa: F401 imported but unused -except ImportError: +except ModuleNotFoundError: Z3CPT_AVAILABLE = False diff --git a/src/z3c/form/browser/widget.py b/src/z3c/form/browser/widget.py index 75d5680..f900310 100644 --- a/src/z3c/form/browser/widget.py +++ b/src/z3c/form/browser/widget.py @@ -153,7 +153,6 @@ def addClass(self, klass: str): # make sure items are not repeated parts = self.klass.split() + klass.split() # Remove duplicates and keep order. - # Dictionaries are ordered in Python 3.7+ parts = list(dict.fromkeys(parts)) self.klass = " ".join(parts) @@ -224,14 +223,12 @@ class HTMLInputWidget(HTMLFormElement): @property def _html_attributes(self) -> list: attributes = super()._html_attributes - attributes.extend( - [ - "readonly", - "alt", - "accesskey", - "onselect", - ] - ) + attributes.extend([ + "readonly", + "alt", + "accesskey", + "onselect", + ]) return attributes @@ -247,14 +244,12 @@ class HTMLTextInputWidget(HTMLInputWidget): @property def _html_attributes(self) -> list: attributes = super()._html_attributes - attributes.extend( - [ - "size", - "maxlength", - "placeholder", - "autocapitalize", - ] - ) + attributes.extend([ + "size", + "maxlength", + "placeholder", + "autocapitalize", + ]) return attributes @@ -270,15 +265,13 @@ class HTMLTextAreaWidget(HTMLFormElement): @property def _html_attributes(self) -> list: attributes = super()._html_attributes - attributes.extend( - [ - "rows", - "cols", - "readonly", - "accesskey", - "onselect", - ] - ) + attributes.extend([ + "rows", + "cols", + "readonly", + "accesskey", + "onselect", + ]) return attributes @@ -291,12 +284,10 @@ class HTMLSelectWidget(HTMLFormElement): @property def _html_attributes(self) -> list: attributes = super()._html_attributes - attributes.extend( - [ - "multiple", - "size", - ] - ) + attributes.extend([ + "multiple", + "size", + ]) return attributes diff --git a/src/z3c/form/button.py b/src/z3c/form/button.py index b02ed71..c171377 100644 --- a/src/z3c/form/button.py +++ b/src/z3c/form/button.py @@ -167,7 +167,7 @@ def __call__(self, form, action): return self.func(form, action) def __repr__(self): - return '<{} for {!r}>'.format(self.__class__.__name__, self.button) + return f'<{self.__class__.__name__} for {self.button!r}>' def handler(button): diff --git a/src/z3c/form/button.rst b/src/z3c/form/button.rst index 1d5a16b..95b938a 100644 --- a/src/z3c/form/button.rst +++ b/src/z3c/form/button.rst @@ -442,7 +442,7 @@ First, you are able to add button managers: >>> bm2 = button.Buttons(button.Button('help', title='Help')) >>> bm1 + bm2 - Buttons([...]) + Buttons(...) >>> list(bm1 + bm2) ['apply', 'cancel', 'help'] diff --git a/src/z3c/form/contentprovider.rst b/src/z3c/form/contentprovider.rst index 68ada7c..bba286e 100644 --- a/src/z3c/form/contentprovider.rst +++ b/src/z3c/form/contentprovider.rst @@ -284,7 +284,7 @@ To enable form updating, all widget adapters must be registered:: >>> personForm.update() >>> personForm.widgets - FieldWidgetsAndProviders([...]) + FieldWidgetsAndProviders(...) Let's render the form:: diff --git a/src/z3c/form/datamanager.py b/src/z3c/form/datamanager.py index 84d963f..56bb86c 100644 --- a/src/z3c/form/datamanager.py +++ b/src/z3c/form/datamanager.py @@ -11,11 +11,7 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -"""Widget Framework Implementation - -$Id$ -""" -__docformat__ = "reStructuredText" +"""Widget Framework Implementation.""" import zope.component import zope.interface @@ -35,7 +31,7 @@ try: import persistent.mapping ALLOWED_DATA_CLASSES.append(persistent.mapping.PersistentMapping) -except ImportError: +except ModuleNotFoundError: pass diff --git a/src/z3c/form/field.py b/src/z3c/form/field.py index 8646417..91682aa 100644 --- a/src/z3c/form/field.py +++ b/src/z3c/form/field.py @@ -83,7 +83,7 @@ def __init__(self, field, name=None, prefix='', mode=None, interface=None, self.showDefault = showDefault def __repr__(self): - return '<{} {!r}>'.format(self.__class__.__name__, self.__name__) + return f'<{self.__class__.__name__} {self.__name__!r}>' @zope.interface.implementer(interfaces.IFields) diff --git a/src/z3c/form/field.rst b/src/z3c/form/field.rst index 0b6cc2e..23fabe7 100644 --- a/src/z3c/form/field.rst +++ b/src/z3c/form/field.rst @@ -529,7 +529,7 @@ When a widget is added to the widget manager, it is located: >>> lname.__name__ 'lastName' >>> lname.__parent__ - FieldWidgets([...]) + FieldWidgets(...) All widgets created by this widget manager are context aware: diff --git a/src/z3c/form/form.rst b/src/z3c/form/form.rst index 0629dc3..6eb936f 100644 --- a/src/z3c/form/form.rst +++ b/src/z3c/form/form.rst @@ -163,7 +163,7 @@ The widget manager is then stored in the ``widgets`` attribute as promised by the ``IForm`` interface: >>> addForm.widgets - FieldWidgets([...]) + FieldWidgets(...) The widget manager will have four widgets, one for each field: diff --git a/src/z3c/form/group.rst b/src/z3c/form/group.rst index ab854b5..d7769c6 100644 --- a/src/z3c/form/group.rst +++ b/src/z3c/form/group.rst @@ -191,7 +191,7 @@ Let's now submit the form, but forgetting to enter the address: >>> request = testing.TestRequest(form={ ... 'form.widgets.firstName': 'Stephan', ... 'form.widgets.lastName': 'Richter', - ... 'form.widgets.license': 'MA 40387', + ... 'form.widgets.license': 'MA 40487', ... 'form.widgets.model': 'BMW', ... 'form.widgets.make': '325', ... 'form.widgets.year': '2005', @@ -226,7 +226,7 @@ So what happens, if errors happen inside and outside a group? >>> request = testing.TestRequest(form={ ... 'form.widgets.firstName': 'Stephan', - ... 'form.widgets.license': 'MA 40387', + ... 'form.widgets.license': 'MA 40487', ... 'form.widgets.model': 'BMW', ... 'form.widgets.make': '325', ... 'form.widgets.year': '2005', @@ -271,7 +271,7 @@ Let's now successfully complete the add form. >>> request = testing.TestRequest(form={ ... 'form.widgets.firstName': 'Stephan', ... 'form.widgets.lastName': 'Richter', - ... 'form.widgets.license': 'MA 40387', + ... 'form.widgets.license': 'MA 40487', ... 'form.widgets.address': '10 Main St, Maynard, MA', ... 'form.widgets.model': 'BMW', ... 'form.widgets.make': '325', @@ -290,7 +290,7 @@ The object is now added to the container and all attributes should be set: >>> reg.lastName 'Richter' >>> reg.license - 'MA 40387' + 'MA 40487' >>> reg.address '10 Main St, Maynard, MA' >>> reg.model @@ -342,7 +342,7 @@ After updating the form, we can render the HTML: + value="MA 40487" />
@@ -390,7 +390,7 @@ The behavior when an error occurs is identical to that of the add form: >>> request = testing.TestRequest(form={ ... 'form.widgets.firstName': 'Stephan', ... 'form.widgets.lastName': 'Richter', - ... 'form.widgets.license': 'MA 40387', + ... 'form.widgets.license': 'MA 40487', ... 'form.widgets.model': 'BMW', ... 'form.widgets.make': '325', ... 'form.widgets.year': '2005', @@ -437,7 +437,7 @@ Let's now complete the form successfully: >>> request = testing.TestRequest(form={ ... 'form.widgets.firstName': 'Stephan', ... 'form.widgets.lastName': 'Richter', - ... 'form.widgets.license': 'MA 4038765', + ... 'form.widgets.license': 'MA 4048765', ... 'form.widgets.address': '11 Main St, Maynard, MA', ... 'form.widgets.model': 'Ford', ... 'form.widgets.make': 'F150', @@ -461,7 +461,7 @@ and the data are correctly updated: >>> reg.lastName 'Richter' >>> reg.license - 'MA 4038765' + 'MA 4048765' >>> reg.address '11 Main St, Maynard, MA' >>> reg.model @@ -534,7 +534,7 @@ Instanciate the form and use a group class and a group instance: + value="MA 4048765" type="text" />
@@ -640,7 +640,7 @@ field which is taken care of with the group. ... 'simple_groupedit.pt', os.path.dirname(tests.__file__)) >>> reg = VehicleRegistration( - ... license='MA 40387', + ... license='MA 40487', ... address='10 Main St, Maynard, MA', ... model='BMW', ... make='325', @@ -664,7 +664,7 @@ the ``owner`` prefix for the fields. + value="MA 40487" />
@@ -725,7 +725,7 @@ Richter gave his BMW to Paul Carduner because he is such a nice guy. >>> request = testing.TestRequest(form={ ... 'form.widgets.owner.firstName': 'Paul', ... 'form.widgets.owner.lastName': 'Carduner', - ... 'form.widgets.license': 'MA 4038765', + ... 'form.widgets.license': 'MA 4048765', ... 'form.widgets.address': 'Berkeley', ... 'form.widgets.model': 'BMW', ... 'form.widgets.make': '325', @@ -748,7 +748,7 @@ Now the owner object should have updated fields. >>> reg.owner.lastName 'Carduner' >>> reg.license - 'MA 4038765' + 'MA 4048765' >>> reg.address 'Berkeley' >>> reg.model @@ -787,7 +787,7 @@ The group can contains groups. Let's adapt the previous RegistrationEditForm: ... 'simple_nested_groupedit.pt', os.path.dirname(tests.__file__)) >>> reg = VehicleRegistration( - ... license='MA 40387', + ... license='MA 40487', ... address='10 Main St, Maynard, MA', ... model='BMW', ... make='325', @@ -805,7 +805,7 @@ Richter gave his BMW to Paul Carduner because he is such a nice guy. >>> request = testing.TestRequest(form={ ... 'form.widgets.owner.firstName': 'Paul', ... 'form.widgets.owner.lastName': 'Carduner', - ... 'form.widgets.license': 'MA 4038765', + ... 'form.widgets.license': 'MA 4048765', ... 'form.widgets.address': 'Berkeley', ... 'form.widgets.model': 'BMW', ... 'form.widgets.make': '325', @@ -828,7 +828,7 @@ Now the owner object should have updated fields. >>> reg.owner.lastName 'Carduner' >>> reg.license - 'MA 4038765' + 'MA 4048765' >>> reg.address 'Berkeley' >>> reg.model @@ -903,7 +903,7 @@ Group instance in nested group Let's also test if the Group class can handle group objects as instances: >>> reg = VehicleRegistration( - ... license='MA 40387', + ... license='MA 40487', ... address='10 Main St, Maynard, MA', ... model='BMW', ... make='325', @@ -940,7 +940,7 @@ Update and render: + value="MA 40487" type="text" />
@@ -1000,7 +1000,7 @@ Now test the error handling if just one missing value is given in a group: >>> request = testing.TestRequest(form={ ... 'form.widgets.owner.firstName': 'Paul', ... 'form.widgets.owner.lastName': '', - ... 'form.widgets.license': 'MA 4038765', + ... 'form.widgets.license': 'MA 4048765', ... 'form.widgets.address': 'Berkeley', ... 'form.widgets.model': 'BMW', ... 'form.widgets.make': '325', diff --git a/src/z3c/form/interfaces.py b/src/z3c/form/interfaces.py index 6a52b85..2855fcb 100644 --- a/src/z3c/form/interfaces.py +++ b/src/z3c/form/interfaces.py @@ -738,7 +738,7 @@ def __init__(self, error): self.error = error def __repr__(self): - return '<{} wrapping {!r}>'.format(self.__class__.__name__, self.error) + return f'<{self.__class__.__name__} wrapping {self.error!r}>' class WidgetActionExecutionError(ActionExecutionError): diff --git a/src/z3c/form/testing.py b/src/z3c/form/testing.py index c14bd66..2d03454 100644 --- a/src/z3c/form/testing.py +++ b/src/z3c/form/testing.py @@ -637,7 +637,7 @@ def __init__(self, **kw): def __repr__(self): items = sorted(self.__dict__.items()) return ("<" + self.__class__.__name__ + "\n " - + "\n ".join(["{}: {}".format(key, pprint.pformat(value)) + + "\n ".join([f"{key}: {pprint.pformat(value)}" for key, value in items]) + ">") diff --git a/src/z3c/form/tests/test_doc.py b/src/z3c/form/tests/test_doc.py index 6ac5dd2..b15e695 100644 --- a/src/z3c/form/tests/test_doc.py +++ b/src/z3c/form/tests/test_doc.py @@ -30,12 +30,12 @@ import z3c.pt import z3c.ptcompat # noqa: F401 imported but unused Z3CPT_AVAILABLE = True -except ImportError: +except ModuleNotFoundError: Z3CPT_AVAILABLE = False try: import zope.app.container # noqa: F401 imported but unused -except ImportError: +except ModuleNotFoundError: ADDING_AVAILABLE = False else: ADDING_AVAILABLE = True diff --git a/src/z3c/form/value.py b/src/z3c/form/value.py index c6560e2..0520767 100644 --- a/src/z3c/form/value.py +++ b/src/z3c/form/value.py @@ -34,7 +34,7 @@ def get(self): return self.value def __repr__(self): - return '<{} {!r}>'.format(self.__class__.__name__, self.value) + return f'<{self.__class__.__name__} {self.value!r}>' @zope.interface.implementer(interfaces.IValue) @@ -48,7 +48,7 @@ def get(self): return self.func(self) def __repr__(self): - return '<{} {!r}>'.format(self.__class__.__name__, self.get()) + return f'<{self.__class__.__name__} {self.get()!r}>' class ValueFactory: diff --git a/src/z3c/form/widget.py b/src/z3c/form/widget.py index 01faae7..38cf2b3 100644 --- a/src/z3c/form/widget.py +++ b/src/z3c/form/widget.py @@ -174,7 +174,7 @@ def __call__(self): return layout(self) def __repr__(self): - return '<{} {!r}>'.format(self.__class__.__name__, self.name) + return f'<{self.__class__.__name__} {self.name!r}>' @zope.interface.implementer(interfaces.ISequenceWidget) @@ -606,7 +606,7 @@ def __init__(self, widget): self.widget = widget def __repr__(self): - return '<{} {!r}>'.format(self.__class__.__name__, self.widget) + return f'<{self.__class__.__name__} {self.widget!r}>' @zope.interface.implementer_only(interfaces.IAfterWidgetUpdateEvent) diff --git a/tox.ini b/tox.ini index 205b9c4..155aa04 100644 --- a/tox.ini +++ b/tox.ini @@ -3,12 +3,13 @@ [tox] minversion = 3.18 envlist = + release-check lint - py37 py38 py39 py310 py311 + py312 docs coverage @@ -17,26 +18,42 @@ usedevelop = true package = wheel wheel_build_env = .pkg deps = + setuptools < 69 zope.testrunner +setenv = + py312: VIRTUALENV_PIP=23.1.2 + py312: PIP_REQUIRE_VIRTUALENV=0 commands = zope-testrunner --test-path=src {posargs:-vc} extras = test -[testenv:lint] +[testenv:release-check] +description = ensure that the distribution is ready to release basepython = python3 skip_install = true -commands = - isort --check-only --diff {toxinidir}/src {toxinidir}/setup.py - flake8 src setup.py - check-manifest - check-python-versions deps = + twine + build check-manifest - check-python-versions >= 0.19.1 + check-python-versions >= 0.20.0 wheel - flake8 +commands_pre = +commands = + check-manifest + check-python-versions --only setup.py,tox.ini,.github/workflows/tests.yml + python -m build --sdist --no-isolation + twine check dist/* + +[testenv:lint] +basepython = python3 +skip_install = true +deps = isort + flake8 +commands = + isort --check-only --diff {toxinidir}/src {toxinidir}/setup.py + flake8 src setup.py [testenv:isort-apply] basepython = python3 @@ -67,7 +84,7 @@ commands = mkdir -p {toxinidir}/parts/htmlcov coverage run -m zope.testrunner --test-path=src {posargs:-vc} coverage html --ignore-errors - coverage report --ignore-errors --show-missing --fail-under=95 + coverage report --show-missing --fail-under=95 [coverage:run] branch = True @@ -75,6 +92,7 @@ source = z3c.form [coverage:report] precision = 2 +ignore_errors = True exclude_lines = pragma: no cover pragma: nocover