diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0e45a78..0474d6a 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"] - - ["pypy-3.9", "pypy3"] - - ["3.9", "coverage"] + - ["3.12", "py312"] + - ["pypy-3.10", "pypy3"] + - ["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 32492b6..aed9641 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 = "fd874ae4" +commit-id = "b1221c3c" [python] with-windows = false @@ -10,6 +10,7 @@ with-pypy = true with-future-python = false with-sphinx-doctests = false with-macos = false +with-docs = false [tox] use-flake8 = true diff --git a/CHANGES.rst b/CHANGES.rst index 2f1acd6..ad74480 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,10 +2,12 @@ Changes ========= -5.1 (unreleased) +6.0 (unreleased) ================ -- Nothing changed yet. +- Add support for Python 3.12. + +- Drop support for Python 3.7. 5.0 (2023-01-19) diff --git a/setup.cfg b/setup.cfg index d18b07e..064a6fe 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 32d1a92..823553d 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ def read(*rnames): setup( name='zope.browserpage', - version='5.1.dev0', + version='6.0.dev0', url='https://github.com/zopefoundation/zope.browserpage', author='Zope Foundation and Contributors', author_email='zope-dev@zope.org', @@ -45,11 +45,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', 'Programming Language :: Python :: Implementation :: PyPy', 'Operating System :: OS Independent', @@ -62,7 +62,7 @@ def read(*rnames): packages=find_packages('src'), package_dir={'': 'src'}, namespace_packages=['zope'], - python_requires='>=3.7', + python_requires='>=3.8', install_requires=[ 'setuptools', 'zope.tal >= 4.2.0', diff --git a/src/zope/browserpage/metaconfigure.py b/src/zope/browserpage/metaconfigure.py index b87c1aa..c031f67 100644 --- a/src/zope/browserpage/metaconfigure.py +++ b/src/zope/browserpage/metaconfigure.py @@ -43,15 +43,14 @@ def _fallbackMenuItemDirective(_context, *args, **kwargs): import warnings warnings.warn_explicit( 'Page directive used with "menu" argument, while "zope.browsermenu" ' - 'package is not installed. Doing nothing.', - UserWarning, + 'package is not installed. Doing nothing.', UserWarning, _context.info.file, _context.info.line) return [] try: from zope.browsermenu.metaconfigure import menuItemDirective -except ImportError: # pragma: no cover +except ModuleNotFoundError: # pragma: no cover menuItemDirective = _fallbackMenuItemDirective # There are three cases we want to suport: @@ -113,10 +112,18 @@ def _norm_template(_context, template): return template -def page(_context, name, permission, for_=Interface, - layer=IDefaultBrowserLayer, template=None, class_=None, - allowed_interface=None, allowed_attributes=None, - attribute='__call__', menu=None, title=None): +def page(_context, + name, + permission, + for_=Interface, + layer=IDefaultBrowserLayer, + template=None, + class_=None, + allowed_interface=None, + allowed_attributes=None, + attribute='__call__', + menu=None, + title=None): _handle_menu(_context, menu, title, [for_], name, permission, layer) required = {} @@ -141,8 +148,7 @@ def page(_context, name, permission, for_=Interface, if attribute != '__call__': if not hasattr(class_, attribute): raise ConfigurationError( - "The provided class doesn't have the specified attribute " - ) + "The provided class doesn't have the specified attribute ") if template: # class and template new_class = SimpleViewClass(template, bases=(class_, ), name=name) @@ -150,7 +156,10 @@ def page(_context, name, permission, for_=Interface, cdict = {} cdict['__name__'] = name cdict['__page_attribute__'] = attribute - new_class = type(class_.__name__, (class_, simple,), cdict) + new_class = type(class_.__name__, ( + class_, + simple, + ), cdict) if hasattr(class_, '__implements__'): classImplements(new_class, IBrowserPublisher) @@ -169,16 +178,16 @@ def page(_context, name, permission, for_=Interface, _handle_for(_context, for_) new_class._simple__whitelist = ( - set(required) - - {attribute, 'browserDefault', '__call__', 'publishTraverse'}) + set(required) - + {attribute, 'browserDefault', '__call__', 'publishTraverse'}) defineChecker(new_class, Checker(required)) _context.action( discriminator=('view', (for_, layer), name, IBrowserRequest), callable=handler, - args=('registerAdapter', - new_class, (for_, layer), Interface, name, _context.info), + args=('registerAdapter', new_class, (for_, layer), Interface, name, + _context.info), ) @@ -187,30 +196,45 @@ def page(_context, name, permission, for_=Interface, # Note that a class might want to access one of the defined # templates. If it does though, it should use getMultiAdapter. + class pages: - def __init__(self, _context, permission, for_=Interface, - layer=IDefaultBrowserLayer, class_=None, - allowed_interface=None, allowed_attributes=None): + def __init__(self, + _context, + permission, + for_=Interface, + layer=IDefaultBrowserLayer, + class_=None, + allowed_interface=None, + allowed_attributes=None): self.opts = dict( - for_=for_, permission=permission, - layer=layer, class_=class_, + for_=for_, + permission=permission, + layer=layer, + class_=class_, allowed_interface=allowed_interface, allowed_attributes=allowed_attributes, ) - def page(self, _context, name, attribute='__call__', template=None, - menu=None, title=None): + def page(self, + _context, + name, + attribute='__call__', + template=None, + menu=None, + title=None): return page(_context, name=name, attribute=attribute, template=template, - menu=menu, title=title, + menu=menu, + title=title, **(self.opts)) def __call__(self): return () + # view (named view with pages) # This is a different case. We actually build a class with attributes @@ -221,10 +245,18 @@ class view: default = None - def __init__(self, _context, permission, for_=Interface, - name='', layer=IDefaultBrowserLayer, class_=None, - allowed_interface=None, allowed_attributes=None, - menu=None, title=None, provides=Interface): + def __init__(self, + _context, + permission, + for_=Interface, + name='', + layer=IDefaultBrowserLayer, + class_=None, + allowed_interface=None, + allowed_attributes=None, + menu=None, + title=None, + provides=Interface): _handle_menu(_context, menu, title, [for_], name, permission, layer) @@ -253,8 +285,8 @@ def defaultPage(self, _context, name): return () def __call__(self): - (_context, name, (for_, layer), permission, class_, - allowed_interface, allowed_attributes) = self.args + (_context, name, (for_, layer), permission, class_, allowed_interface, + allowed_attributes) = self.args required = {} @@ -269,8 +301,7 @@ def __call__(self): cdict[attribute] = cdict[pname] else: if not hasattr(class_, attribute): - raise ConfigurationError("Undefined attribute", - attribute) + raise ConfigurationError("Undefined attribute", attribute) attribute = attribute or pname required[pname] = permission @@ -280,8 +311,11 @@ def __call__(self): # This should go away, but noone seems to remember what to do. :-( if hasattr(class_, 'publishTraverse'): - def publishTraverse(self, request, name, - pages=pages, getattr=getattr): + def publishTraverse(self, + request, + name, + pages=pages, + getattr=getattr): if name in pages: return getattr(self, pages[name]) @@ -293,8 +327,12 @@ def publishTraverse(self, request, name, return m(request, name) else: - def publishTraverse(self, request, name, - pages=pages, getattr=getattr): + + def publishTraverse(self, + request, + name, + pages=pages, + getattr=getattr): if name in pages: return getattr(self, pages[name]) @@ -310,15 +348,12 @@ def publishTraverse(self, request, name, if self.default or self.pages: _default = self.default or self.pages[0][0] cdict['browserDefault'] = ( - lambda self, request, default=_default: - (self, (default, )) - ) + lambda self, request, default=_default: (self, + (default, ))) elif providesCallable(class_): - cdict['browserDefault'] = ( - lambda self, request: (self, ()) - ) + cdict['browserDefault'] = (lambda self, request: (self, ())) - bases = (simple,) if class_ is None else (class_, simple) + bases = (simple, ) if class_ is None else (class_, simple) try: cname = str(name) @@ -340,22 +375,24 @@ def publishTraverse(self, request, name, defineChecker(newclass, Checker(required)) if self.provides is not None: - _context.action( - discriminator=None, - callable=provideInterface, - args=('', self.provides) - ) + _context.action(discriminator=None, + callable=provideInterface, + args=('', self.provides)) _context.action( discriminator=('view', (for_, layer), name, self.provides), callable=handler, - args=('registerAdapter', - newclass, (for_, layer), self.provides, name, - _context.info), + args=('registerAdapter', newclass, (for_, layer), self.provides, + name, _context.info), ) -def _handle_menu(_context, menu, title, for_, name, permission, +def _handle_menu(_context, + menu, + title, + for_, + name, + permission, layer=IDefaultBrowserLayer): if not menu and not title: # Neither of them @@ -372,9 +409,13 @@ def _handle_menu(_context, menu, title, for_, name, permission, "Menus can be specified only for single-view, not for " "multi-views.") - return menuItemDirective( - _context, menu, for_[0], '@@' + name, title, - permission=permission, layer=layer) + return menuItemDirective(_context, + menu, + for_[0], + '@@' + name, + title, + permission=permission, + layer=layer) def _handle_permission(_context, permission): @@ -389,11 +430,9 @@ def _handle_allowed_interface(_context, allowed_interface, permission, # Allow access for all names defined by named interfaces if allowed_interface: for i in allowed_interface: - _context.action( - discriminator=None, - callable=provideInterface, - args=(None, i) - ) + _context.action(discriminator=None, + callable=provideInterface, + args=(None, i)) for name in i: required[name] = permission @@ -409,11 +448,9 @@ def _handle_allowed_attributes(_context, allowed_attributes, permission, def _handle_for(_context, for_): if for_ is not None: - _context.action( - discriminator=None, - callable=provideInterface, - args=('', for_) - ) + _context.action(discriminator=None, + callable=provideInterface, + args=('', for_)) @implementer(IBrowserPublisher) @@ -459,11 +496,9 @@ def providesCallable(class_): def expressiontype(_context, name, handler): - _context.action( - discriminator=("tales:expressiontype", name), - callable=registerType, - args=(name, handler) - ) + _context.action(discriminator=("tales:expressiontype", name), + callable=registerType, + args=(name, handler)) def registerType(name, handler): @@ -480,7 +515,7 @@ def clear(): try: from zope.testing.cleanup import addCleanUp -except ImportError: # pragma: no cover +except ModuleNotFoundError: # pragma: no cover pass else: addCleanUp(clear) diff --git a/src/zope/browserpage/metadirectives.py b/src/zope/browserpage/metadirectives.py index fb30cf1..16d3b06 100644 --- a/src/zope/browserpage/metadirectives.py +++ b/src/zope/browserpage/metadirectives.py @@ -26,7 +26,7 @@ try: from zope.browsermenu.field import MenuField -except ImportError: # pragma: no cover +except ModuleNotFoundError: # pragma: no cover # avoid hard dependency on zope.browsermenu MenuField = TextLine @@ -40,23 +40,19 @@ class IPagesDirective(IBasicViewInformation): 'allowed_attributes', and 'allowed_interface' attributes. """ - for_ = GlobalObject( - title="The interface or class this view is for.", - required=False - ) + for_ = GlobalObject(title="The interface or class this view is for.", + required=False) layer = GlobalObject( title="The request interface or class this view is for.", description="Defaults to " - "zope.publisher.interfaces.browser.IDefaultBrowserLayer.", - required=False - ) + "zope.publisher.interfaces.browser.IDefaultBrowserLayer.", + required=False) permission = Permission( title="Permission", description="The permission needed to use the view.", - required=True - ) + required=True) class IViewDirective(IPagesDirective): @@ -67,10 +63,8 @@ class IViewDirective(IPagesDirective): traversing to the view name and then traversing to the page name. """ - for_ = GlobalInterface( - title="The interface this view is for.", - required=False - ) + for_ = GlobalInterface(title="The interface this view is for.", + required=False) name = TextLine( title="The name of the view.", @@ -79,9 +73,8 @@ class IViewDirective(IPagesDirective): default='', ) - menu = MenuField( - title="The browser menu to include the page (view) in.", - description=""" + menu = MenuField(title="The browser menu to include the page (view) in.", + description=""" Many views are included in menus. It's convenient to name the menu in the page directive, rather than having to give a separate menuItem directive. 'zmi_views' is the menu most often @@ -89,19 +82,16 @@ class IViewDirective(IPagesDirective): This attribute will only work if zope.browsermenu is installed. """, - required=False - ) + required=False) - title = MessageID( - title="The browser menu label for the page (view)", - description=""" + title = MessageID(title="The browser menu label for the page (view)", + description=""" This attribute must be supplied if a menu attribute is supplied. This attribute will only work if zope.browsermenu is installed. """, - required=False - ) + required=False) provides = GlobalInterface( title="The interface this view provides.", @@ -118,33 +108,28 @@ class IViewPageSubdirective(Interface): Subdirective to IViewDirective. """ - name = TextLine( - title="The name of the page (view)", - description=""" + name = TextLine(title="The name of the page (view)", + description=""" The name shows up in URLs/paths. For example 'foo' or 'foo.html'. This attribute is required unless you use the subdirective 'page' to create sub views. If you do not have sub pages, it is common to use an extension for the view name such as '.html'. If you do have sub pages and you want to provide a view name, you shouldn't use extensions.""", - required=True - ) + required=True) attribute = PythonIdentifier( title="The name of the view attribute implementing the page.", description=""" This refers to the attribute (method) on the view that is implementing a specific sub page.""", - required=False - ) + required=False) - template = Path( - title="The name of a template that implements the page.", - description=""" + template = Path(title="The name of a template that implements the page.", + description=""" Refers to a file containing a page template (should end in extension '.pt' or '.html').""", - required=False - ) + required=False) class IViewDefaultPageSubdirective(Interface): @@ -152,15 +137,13 @@ class IViewDefaultPageSubdirective(Interface): Subdirective to IViewDirective. """ - name = TextLine( - title="The name of the page that is the default.", - description=""" + name = TextLine(title="The name of the page that is the default.", + description=""" The named page will be used as the default if no name is specified explicitly in the path. If no defaultPage directive is supplied, the default page will be the first page listed.""", - required=True - ) + required=True) class IPagesPageSubdirective(IViewPageSubdirective): @@ -168,28 +151,24 @@ class IPagesPageSubdirective(IViewPageSubdirective): Subdirective to IPagesDirective """ - menu = MenuField( - title="The browser menu to include the page (view) in.", - description=""" + menu = MenuField(title="The browser menu to include the page (view) in.", + description=""" Many views are included in menus. It's convenient to name the menu in the page directive, rather than having to give a separate menuItem directive. This attribute will only work if zope.browsermenu is installed. """, - required=False - ) + required=False) - title = MessageID( - title="The browser menu label for the page (view)", - description=""" + title = MessageID(title="The browser menu label for the page (view)", + description=""" This attribute must be supplied if a menu attribute is supplied. This attribute will only work if zope.browsermenu is installed. """, - required=False - ) + required=False) class IPageDirective(IPagesDirective, IPagesPageSubdirective): @@ -209,12 +188,9 @@ class IExpressionTypeDirective(Interface): title="Name", description="""Name of the expression. This will also be used as the prefix in actual TALES expressions.""", - required=True - ) + required=True) - handler = GlobalObject( - title="Handler", - description="""Handler is class that implements + handler = GlobalObject(title="Handler", + description="""Handler is class that implements zope.tales.interfaces.ITALESExpression.""", - required=True - ) + required=True) diff --git a/tox.ini b/tox.ini index cef2f9c..7cc875d 100644 --- a/tox.ini +++ b/tox.ini @@ -3,37 +3,55 @@ [tox] minversion = 3.18 envlist = + release-check lint - py37 py38 py39 py310 py311 + py312 pypy3 coverage [testenv] usedevelop = true +package = wheel +wheel_build_env = .pkg deps = + setuptools < 69 +setenv = + py312: VIRTUALENV_PIP=23.1.2 + py312: PIP_REQUIRE_VIRTUALENV=0 commands = zope-testrunner --test-path=src {posargs:-vc} extras = test +[testenv:release-check] +description = ensure that the distribution is ready to release +basepython = python3 +skip_install = true +deps = + twine + build + check-manifest + check-python-versions >= 0.20.0 + wheel +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 - check-manifest - check-python-versions -deps = - check-manifest - check-python-versions >= 0.19.1 - wheel - flake8 - isort [testenv:isort-apply] basepython = python3 @@ -54,7 +72,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=99.47 + coverage report --show-missing --fail-under=99.47 [coverage:run] branch = True @@ -62,6 +80,7 @@ source = zope.browserpage [coverage:report] precision = 2 +ignore_errors = True exclude_lines = pragma: no cover pragma: nocover