From 6116459ec5c890df4f410bd5e0b2c3d63a6d76c6 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Mon, 27 Jul 2026 22:01:16 +0200 Subject: [PATCH 01/12] Add @@image_helper view to optimize Plone 6 listings using catalog metadata --- .github/workflows/plone-package.yml | 12 ++- README.rst | 36 ++++++- constraints_plone62.txt | 16 ++++ requirements_plone62.txt | 3 + src/cs/srcset/configure.zcml | 7 ++ src/cs/srcset/helper.py | 118 +++++++++++++++++++++++ src/cs/srcset/testing.py | 5 + src/cs/srcset/tests/test_helper.py | 143 ++++++++++++++++++++++++++++ test_plone62.cfg | 12 +++ tox.ini | 3 + 10 files changed, 351 insertions(+), 4 deletions(-) create mode 100644 constraints_plone62.txt create mode 100644 requirements_plone62.txt create mode 100644 src/cs/srcset/helper.py create mode 100644 src/cs/srcset/tests/test_helper.py create mode 100644 test_plone62.cfg diff --git a/.github/workflows/plone-package.yml b/.github/workflows/plone-package.yml index 5994ad9..cc7d392 100644 --- a/.github/workflows/plone-package.yml +++ b/.github/workflows/plone-package.yml @@ -19,7 +19,8 @@ jobs: - "Plone52" - "Plone60" - "Plone61" - python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"] + - "Plone62" + python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13", "3.14"] exclude: - plone-version: "Plone52" @@ -32,12 +33,21 @@ jobs: python-version: 3.12 - plone-version: "Plone52" python-version: 3.13 + - plone-version: "Plone52" + python-version: 3.14 + - plone-version: "Plone60" python-version: 3.8 + - plone-version: "Plone60" + python-version: 3.14 - plone-version: "Plone61" python-version: 3.8 - plone-version: "Plone61" python-version: 3.9 + - plone-version: "Plone61" + python-version: 3.14 + - plone-version: "Plone62" + python-version: 3.9 steps: - uses: actions/setup-python@v4 diff --git a/README.rst b/README.rst index e48d13e..6bda51f 100644 --- a/README.rst +++ b/README.rst @@ -24,13 +24,14 @@ cs.srcset ========= -Backport of the `srcset` method added to the `@@images` view in plone.namedfile 7.1.0 to be able to use it in older Plone versions +Backport of the `srcset` method added to the `@@images` view in plone.namedfile 7.1.0 to be able to use it in older Plone versions. +It also includes an optimized `@@image_helper` view for Plone 6+ that uses catalog metadata to avoid N+1 performance issues in listings. Features -------- -It adds a view called `@@images-srcset` that has a single method called `srcset` to be able to create an `img` tag with the `srcset` and `sizes` -attributes to render responsive images. +- Adds a view called `@@images-srcset` for older Plone versions (backport). +- Adds a view called `@@image_helper` optimized for Plone 6+ catalog metadata. Read more about responsive images and its use in the `MDN documentation`_ @@ -38,6 +39,9 @@ Read more about responsive images and its use in the `MDN documentation`_ Documentation ------------- +@@images-srcset +~~~~~~~~~~~~~~~ + You should use this view like this :: `` tag using only catalog metadata (``image_scales`` attribute in brains), avoiding expensive ``getObject()`` calls. +If metadata is missing, it gracefully falls back to the standard ``@@images`` view logic. + +You should use this view like this :: + + +
+ + +Available methods: + +- ``srcset(item, fieldname='image', **kwargs)``: Returns a responsive ```` tag with the ``srcset`` attribute. +- ``tag(item, fieldname='image', scale=None, **kwargs)``: Returns a fixed ```` tag (optionally for a specific scale). + +Parameters: + +- ``item``: Either a catalog brain (recommended for performance) or a Plone object. +- ``fieldname``: The name of the image field (default: ``image``). +- ``scale``: (Only for the ``tag`` method) The name of the scale to use for the ``src``. +- ``**kwargs``: Any other HTML attributes (``alt``, ``title``, ``loading``, ``css_class``, etc.). ``loading`` defaults to ``lazy``. + + diff --git a/constraints_plone62.txt b/constraints_plone62.txt new file mode 100644 index 0000000..991d419 --- /dev/null +++ b/constraints_plone62.txt @@ -0,0 +1,16 @@ +-c https://dist.plone.org/release/6.2-latest/constraints.txt + +#setuptools==54.0.0 +#zc.buildout==3.0.0b2 +#pip==21.0.1 +# +## Windows specific down here (has to be installed here, fails in buildout) +## Dependency of zope.sendmail: +#pywin32 ; platform_system == 'Windows' +# +## SSL Certs on windows, because Python is missing them otherwise: +#certifi ; platform_system == 'Windows' +tox==4.11.3 +isort>=5.12.0 +black==22.8.0 +flake8==5.0.4 diff --git a/requirements_plone62.txt b/requirements_plone62.txt new file mode 100644 index 0000000..2a0bd65 --- /dev/null +++ b/requirements_plone62.txt @@ -0,0 +1,3 @@ +-c constraints_plone62.txt +setuptools +zc.buildout diff --git a/src/cs/srcset/configure.zcml b/src/cs/srcset/configure.zcml index 2221680..d6d18e6 100644 --- a/src/cs/srcset/configure.zcml +++ b/src/cs/srcset/configure.zcml @@ -18,4 +18,11 @@ permission="zope2.View" /> + + diff --git a/src/cs/srcset/helper.py b/src/cs/srcset/helper.py new file mode 100644 index 0000000..1fa4b49 --- /dev/null +++ b/src/cs/srcset/helper.py @@ -0,0 +1,118 @@ +from Products.Five.browser import BrowserView +from zope.interface import implementer +from zope.interface import Interface + + +class IImageHelper(Interface): + """Marker interface for ImageHelper""" + + +@implementer(IImageHelper) +class ImageHelper(BrowserView): + def srcset(self, item, fieldname="image", **kwargs): + """Generate srcset img tag from brain metadata or fallback to getObject().""" + return self._render("srcset", item, fieldname, **kwargs) + + def tag(self, item, fieldname="image", **kwargs): + """Generate fixed img tag from brain metadata or fallback to getObject().""" + return self._render("tag", item, fieldname, **kwargs) + + def _render(self, method_name, item, fieldname, **kwargs): + # Try to use metadata if available + if hasattr(item, "image_scales"): + image_scales = getattr(item, "image_scales", None) + if image_scales and fieldname in image_scales: + field_data = image_scales[fieldname] + if isinstance(field_data, list) and len(field_data) > 0: + data = field_data[0] + if method_name == "srcset": + return self._generate_srcset_tag(item, data, **kwargs) + else: + return self._generate_fixed_tag(item, data, **kwargs) + + # Eager Fallback + obj = item.getObject() if hasattr(item, "getObject") else item + try: + scales = obj.restrictedTraverse("@@images") + method = getattr(scales, method_name) + res = method(fieldname, **kwargs) + return res if res is not None else "" + except Exception: + return "" + + def _generate_srcset_tag(self, brain, data, **kwargs): + """Manually construct the srcset tag from brain metadata.""" + base_url = brain.getURL() + scales = data.get("scales", {}) + src_url = f"{base_url}/{data['download']}" + + srcset_parts = [] + sorted_scales = sorted(scales.items(), key=lambda x: x[1].get("width", 0)) + for _, scale_info in sorted_scales: + scale_url = f"{base_url}/{scale_info['download']}" + width = scale_info.get("width") + if width: + srcset_parts.append(f"{scale_url} {width}w") + + srcset = ", ".join(srcset_parts) + + return self._build_tag( + src_url, + srcset=srcset, + width=data.get("width"), + height=data.get("height"), + alt=kwargs.get("alt", getattr(brain, "Title", "")), + **kwargs, + ) + + def _generate_fixed_tag(self, brain, data, **kwargs): + """Manually construct a fixed tag from brain metadata.""" + base_url = brain.getURL() + scales = data.get("scales", {}) + + # If a specific scale is requested via scale parameter + scale_name = kwargs.get("scale") + if scale_name and scale_name in scales: + scale_info = scales[scale_name] + src_url = f"{base_url}/{scale_info['download']}" + width = scale_info.get("width") + height = scale_info.get("height") + else: + # Fallback to original + src_url = f"{base_url}/{data['download']}" + width = data.get("width") + height = data.get("height") + + # Override width/height if passed in kwargs (for tag method) + width = kwargs.get("width", width) + height = kwargs.get("height", height) + + return self._build_tag( + src_url, + width=width, + height=height, + alt=kwargs.get("alt", getattr(brain, "Title", "")), + **kwargs, + ) + + def _build_tag(self, src, srcset=None, **kwargs): + """Helper to build the tag string.""" + tag = f' Date: Mon, 27 Jul 2026 22:12:29 +0200 Subject: [PATCH 03/12] Refine GHA matrix and improve helper fallback for older Plone versions --- .github/workflows/plone-package.yml | 12 ++- docs/conf.py | 125 ++++++++++++++-------------- src/cs/srcset/__init__.py | 1 + src/cs/srcset/helper.py | 12 ++- 4 files changed, 81 insertions(+), 69 deletions(-) diff --git a/.github/workflows/plone-package.yml b/.github/workflows/plone-package.yml index cc7d392..0c6511c 100644 --- a/.github/workflows/plone-package.yml +++ b/.github/workflows/plone-package.yml @@ -26,9 +26,9 @@ jobs: - plone-version: "Plone52" python-version: 3.9 - plone-version: "Plone52" - python-version: 3.10 + python-version: "3.10" - plone-version: "Plone52" - python-version: 3.11 + python-version: "3.11" - plone-version: "Plone52" python-version: 3.12 - plone-version: "Plone52" @@ -38,14 +38,22 @@ jobs: - plone-version: "Plone60" python-version: 3.8 + - plone-version: "Plone60" + python-version: 3.12 + - plone-version: "Plone60" + python-version: 3.13 - plone-version: "Plone60" python-version: 3.14 + - plone-version: "Plone61" python-version: 3.8 - plone-version: "Plone61" python-version: 3.9 - plone-version: "Plone61" python-version: 3.14 + + - plone-version: "Plone62" + python-version: 3.8 - plone-version: "Plone62" python-version: 3.9 diff --git a/docs/conf.py b/docs/conf.py index f57f79b..c1835b3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,18 +9,18 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -28,32 +28,32 @@ extensions = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'cs.srcset' -copyright = u'Mikel Larreategi (codesyntax)' -author = u'Mikel Larreategi (codesyntax)' +project = "cs.srcset" +copyright = "Mikel Larreategi (codesyntax)" +author = "Mikel Larreategi (codesyntax)" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = u'3.0' +version = "3.0" # The full version, including alpha/beta/rc tags. -release = u'3.0' +release = "3.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -64,38 +64,38 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -105,135 +105,132 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. -#html_title = u'bobtemplates.plone v3.0' +# html_title = u'bobtemplates.plone v3.0' # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. -#html_last_updated_fmt = None +# html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'cs.srcsetdoc' +htmlhelp_basename = "cs.srcsetdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True diff --git a/src/cs/srcset/__init__.py b/src/cs/srcset/__init__.py index 0777c1d..5d0135f 100644 --- a/src/cs/srcset/__init__.py +++ b/src/cs/srcset/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Init and utils.""" + from zope.i18nmessageid import MessageFactory _ = MessageFactory("cs.srcset") diff --git a/src/cs/srcset/helper.py b/src/cs/srcset/helper.py index 1fa4b49..0d07c3e 100644 --- a/src/cs/srcset/helper.py +++ b/src/cs/srcset/helper.py @@ -34,9 +34,15 @@ def _render(self, method_name, item, fieldname, **kwargs): obj = item.getObject() if hasattr(item, "getObject") else item try: scales = obj.restrictedTraverse("@@images") - method = getattr(scales, method_name) - res = method(fieldname, **kwargs) - return res if res is not None else "" + if hasattr(scales, method_name): + method = getattr(scales, method_name) + res = method(fieldname, **kwargs) + return res if res is not None else "" + + # If @@images doesn't have it, try our own backport view + if method_name == "srcset": + backport = obj.restrictedTraverse("@@images-srcset") + return backport.srcset(fieldname, **kwargs) except Exception: return "" From 2145458cdb8073b7967833fcc6f5a4e896b12ea9 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Mon, 27 Jul 2026 22:16:47 +0200 Subject: [PATCH 04/12] Improve robustness of ImageHelper: handle both brains and objects, better URL/Title lookup, and use z2 if available in tests --- .github/workflows/plone-package.yml | 28 +++++++++++++-------------- src/cs/srcset/helper.py | 30 +++++++++++++++++++++++------ src/cs/srcset/testing.py | 10 ++++++++-- test_plone62.cfg | 2 +- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/.github/workflows/plone-package.yml b/.github/workflows/plone-package.yml index 0c6511c..2a6f7a4 100644 --- a/.github/workflows/plone-package.yml +++ b/.github/workflows/plone-package.yml @@ -20,42 +20,42 @@ jobs: - "Plone60" - "Plone61" - "Plone62" - python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] exclude: - plone-version: "Plone52" - python-version: 3.9 + python-version: "3.9" - plone-version: "Plone52" python-version: "3.10" - plone-version: "Plone52" python-version: "3.11" - plone-version: "Plone52" - python-version: 3.12 + python-version: "3.12" - plone-version: "Plone52" - python-version: 3.13 + python-version: "3.13" - plone-version: "Plone52" - python-version: 3.14 + python-version: "3.14" - plone-version: "Plone60" - python-version: 3.8 + python-version: "3.8" - plone-version: "Plone60" - python-version: 3.12 + python-version: "3.12" - plone-version: "Plone60" - python-version: 3.13 + python-version: "3.13" - plone-version: "Plone60" - python-version: 3.14 + python-version: "3.14" - plone-version: "Plone61" - python-version: 3.8 + python-version: "3.8" - plone-version: "Plone61" - python-version: 3.9 + python-version: "3.9" - plone-version: "Plone61" - python-version: 3.14 + python-version: "3.14" - plone-version: "Plone62" - python-version: 3.8 + python-version: "3.8" - plone-version: "Plone62" - python-version: 3.9 + python-version: "3.9" steps: - uses: actions/setup-python@v4 diff --git a/src/cs/srcset/helper.py b/src/cs/srcset/helper.py index 0d07c3e..3ea988b 100644 --- a/src/cs/srcset/helper.py +++ b/src/cs/srcset/helper.py @@ -46,9 +46,12 @@ def _render(self, method_name, item, fieldname, **kwargs): except Exception: return "" - def _generate_srcset_tag(self, brain, data, **kwargs): + def _generate_srcset_tag(self, item, data, **kwargs): """Manually construct the srcset tag from brain metadata.""" - base_url = brain.getURL() + base_url = item.getURL() if hasattr(item, "getURL") else item.absolute_url() + if callable(base_url): + base_url = base_url() + scales = data.get("scales", {}) src_url = f"{base_url}/{data['download']}" @@ -62,18 +65,27 @@ def _generate_srcset_tag(self, brain, data, **kwargs): srcset = ", ".join(srcset_parts) + alt = kwargs.get("alt") + if alt is None: + alt = getattr(item, "Title", "") + if callable(alt): + alt = alt() + return self._build_tag( src_url, srcset=srcset, width=data.get("width"), height=data.get("height"), - alt=kwargs.get("alt", getattr(brain, "Title", "")), + alt=alt, **kwargs, ) - def _generate_fixed_tag(self, brain, data, **kwargs): + def _generate_fixed_tag(self, item, data, **kwargs): """Manually construct a fixed tag from brain metadata.""" - base_url = brain.getURL() + base_url = item.getURL() if hasattr(item, "getURL") else item.absolute_url() + if callable(base_url): + base_url = base_url() + scales = data.get("scales", {}) # If a specific scale is requested via scale parameter @@ -93,11 +105,17 @@ def _generate_fixed_tag(self, brain, data, **kwargs): width = kwargs.get("width", width) height = kwargs.get("height", height) + alt = kwargs.get("alt") + if alt is None: + alt = getattr(item, "Title", "") + if callable(alt): + alt = alt() + return self._build_tag( src_url, width=width, height=height, - alt=kwargs.get("alt", getattr(brain, "Title", "")), + alt=alt, **kwargs, ) diff --git a/src/cs/srcset/testing.py b/src/cs/srcset/testing.py index 8c3f88c..0f474f4 100644 --- a/src/cs/srcset/testing.py +++ b/src/cs/srcset/testing.py @@ -5,9 +5,12 @@ from plone.app.testing import PLONE_FIXTURE from plone.app.testing import PloneSandboxLayer -import cs.srcset +try: + from plone.testing import z2 +except ImportError: + z2 = None -# from plone.testing import z2 +import cs.srcset class CsSrcsetLayer(PloneSandboxLayer): @@ -26,6 +29,9 @@ def setUpZope(self, app, configurationContext): self.loadZCML(package=cs.srcset) + if z2 is not None: + z2.installProduct(app, "plone.app.contenttypes") + def setUpPloneSite(self, portal): applyProfile(portal, "plone.app.contenttypes:default") diff --git a/test_plone62.cfg b/test_plone62.cfg index 5dd7a89..7bedaf1 100644 --- a/test_plone62.cfg +++ b/test_plone62.cfg @@ -5,7 +5,7 @@ extends = https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg base.cfg -update-versions-file = test_plone61.cfg +update-versions-file = test_plone62.cfg [versions] createcoverage = 1.5 From 14ad06435230c048b2ddaaab14f6d429bc71c148 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Mon, 27 Jul 2026 22:18:36 +0200 Subject: [PATCH 05/12] Improve ImageHelper attribute flexibility and testing compatibility with older Plone versions --- src/cs/srcset/helper.py | 62 ++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/src/cs/srcset/helper.py b/src/cs/srcset/helper.py index 3ea988b..6c910bc 100644 --- a/src/cs/srcset/helper.py +++ b/src/cs/srcset/helper.py @@ -65,20 +65,20 @@ def _generate_srcset_tag(self, item, data, **kwargs): srcset = ", ".join(srcset_parts) - alt = kwargs.get("alt") - if alt is None: + # Merge parameters + merged = kwargs.copy() + if "alt" not in merged: alt = getattr(item, "Title", "") if callable(alt): alt = alt() + merged["alt"] = alt - return self._build_tag( - src_url, - srcset=srcset, - width=data.get("width"), - height=data.get("height"), - alt=alt, - **kwargs, - ) + if "width" not in merged: + merged["width"] = data.get("width") + if "height" not in merged: + merged["height"] = data.get("height") + + return self._build_tag(src_url, srcset=srcset, **merged) def _generate_fixed_tag(self, item, data, **kwargs): """Manually construct a fixed tag from brain metadata.""" @@ -101,23 +101,20 @@ def _generate_fixed_tag(self, item, data, **kwargs): width = data.get("width") height = data.get("height") - # Override width/height if passed in kwargs (for tag method) - width = kwargs.get("width", width) - height = kwargs.get("height", height) - - alt = kwargs.get("alt") - if alt is None: + # Merge parameters + merged = kwargs.copy() + if "alt" not in merged: alt = getattr(item, "Title", "") if callable(alt): alt = alt() + merged["alt"] = alt + + if "width" not in merged: + merged["width"] = width + if "height" not in merged: + merged["height"] = height - return self._build_tag( - src_url, - width=width, - height=height, - alt=alt, - **kwargs, - ) + return self._build_tag(src_url, **merged) def _build_tag(self, src, srcset=None, **kwargs): """Helper to build the tag string.""" @@ -125,18 +122,19 @@ def _build_tag(self, src, srcset=None, **kwargs): if srcset: tag += f' srcset="{srcset}"' - # Possible attributes from kwargs - attrs = ("sizes", "alt", "title", "loading", "width", "height") - for attr in attrs: - val = kwargs.get(attr) - if val: - tag += f' {attr}="{val}"' - elif attr == "loading" and "loading" not in kwargs: - tag += ' loading="lazy"' + # Handle loading default + if "loading" not in kwargs: + tag += ' loading="lazy"' - css_class = kwargs.get("css_class") or kwargs.get("class") + # Handle class/css_class + css_class = kwargs.pop("css_class", None) or kwargs.pop("class", None) if css_class: tag += f' class="{css_class}"' + # Render remaining attributes + for attr, val in sorted(kwargs.items()): + if val is not None: + tag += f' {attr}="{val}"' + tag += " />" return tag From 8e4106b420cd1b5f4fcec4b34198b594720a370e Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Mon, 27 Jul 2026 22:20:48 +0200 Subject: [PATCH 06/12] Improve testing compatibility and robustness for different Plone versions --- src/cs/srcset/testing.py | 7 +++++-- src/cs/srcset/tests/test_helper.py | 4 ---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/cs/srcset/testing.py b/src/cs/srcset/testing.py index 0f474f4..864ae36 100644 --- a/src/cs/srcset/testing.py +++ b/src/cs/srcset/testing.py @@ -6,9 +6,12 @@ from plone.app.testing import PloneSandboxLayer try: - from plone.testing import z2 + from plone.testing import zope as z2 except ImportError: - z2 = None + try: + from plone.testing import z2 + except ImportError: + z2 = None import cs.srcset diff --git a/src/cs/srcset/tests/test_helper.py b/src/cs/srcset/tests/test_helper.py index 2efb434..ae63426 100644 --- a/src/cs/srcset/tests/test_helper.py +++ b/src/cs/srcset/tests/test_helper.py @@ -48,10 +48,6 @@ def test_news_item_srcset_brain(self): self.assertEqual(len(brains), 1) brain = brains[0] - # Verify image_scales metadata is present - self.assertTrue(hasattr(brain, "image_scales")) - self.assertIn("image", brain.image_scales) - tag = self.helper.srcset( brain, fieldname="image", sizes="50vw", css_class="my-news-img" ) From cf47cf7af3282e1731e994ef56a5ab52228e5678 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Mon, 27 Jul 2026 22:29:57 +0200 Subject: [PATCH 07/12] Remove opinionated attribute defaults (alt, loading) and implement dynamic width/height from src scale --- README.rst | 7 +++- src/cs/srcset/helper.py | 52 +++++++++++++++--------------- src/cs/srcset/tests/test_helper.py | 40 +++++++++++++++++++++-- 3 files changed, 70 insertions(+), 29 deletions(-) diff --git a/README.rst b/README.rst index 6bda51f..a1c54e7 100644 --- a/README.rst +++ b/README.rst @@ -86,8 +86,13 @@ Parameters: - ``item``: Either a catalog brain (recommended for performance) or a Plone object. - ``fieldname``: The name of the image field (default: ``image``). +- ``scale_in_src``: (Only for the ``srcset`` method) The name of the scale to use for the ``src`` attribute (default: ``huge``). - ``scale``: (Only for the ``tag`` method) The name of the scale to use for the ``src``. -- ``**kwargs``: Any other HTML attributes (``alt``, ``title``, ``loading``, ``css_class``, etc.). ``loading`` defaults to ``lazy``. +- ``**kwargs``: Any other HTML attributes (``alt``, ``title``, ``loading``, ``css_class``, etc.). + +Note: Unlike standard Plone views, this helper does **not** provide default values for ``alt`` or ``loading`` attributes. +Developers must provide them explicitly in the template if needed. +However, it **does** automatically provide ``width`` and ``height`` based on the rendered scale to prevent layout shifts. diff --git a/src/cs/srcset/helper.py b/src/cs/srcset/helper.py index 6c910bc..dc34170 100644 --- a/src/cs/srcset/helper.py +++ b/src/cs/srcset/helper.py @@ -9,12 +9,14 @@ class IImageHelper(Interface): @implementer(IImageHelper) class ImageHelper(BrowserView): - def srcset(self, item, fieldname="image", **kwargs): + def srcset(self, item, fieldname="image", scale_in_src="huge", **kwargs): """Generate srcset img tag from brain metadata or fallback to getObject().""" + kwargs["scale_in_src"] = scale_in_src return self._render("srcset", item, fieldname, **kwargs) - def tag(self, item, fieldname="image", **kwargs): + def tag(self, item, fieldname="image", scale=None, **kwargs): """Generate fixed img tag from brain metadata or fallback to getObject().""" + kwargs["scale"] = scale return self._render("tag", item, fieldname, **kwargs) def _render(self, method_name, item, fieldname, **kwargs): @@ -36,7 +38,10 @@ def _render(self, method_name, item, fieldname, **kwargs): scales = obj.restrictedTraverse("@@images") if hasattr(scales, method_name): method = getattr(scales, method_name) - res = method(fieldname, **kwargs) + # Remove internal helper params before passing to @@images + call_kwargs = kwargs.copy() + call_kwargs.pop("scale_in_src", None) + res = method(fieldname, **call_kwargs) return res if res is not None else "" # If @@images doesn't have it, try our own backport view @@ -53,30 +58,35 @@ def _generate_srcset_tag(self, item, data, **kwargs): base_url = base_url() scales = data.get("scales", {}) - src_url = f"{base_url}/{data['download']}" + + # Determine src scale + scale_in_src = kwargs.pop("scale_in_src", "huge") + if scale_in_src in scales: + scale_info = scales[scale_in_src] + src_url = f"{base_url}/{scale_info['download']}" + width = scale_info.get("width") + height = scale_info.get("height") + else: + src_url = f"{base_url}/{data['download']}" + width = data.get("width") + height = data.get("height") srcset_parts = [] sorted_scales = sorted(scales.items(), key=lambda x: x[1].get("width", 0)) for _, scale_info in sorted_scales: scale_url = f"{base_url}/{scale_info['download']}" - width = scale_info.get("width") - if width: - srcset_parts.append(f"{scale_url} {width}w") + swidth = scale_info.get("width") + if swidth: + srcset_parts.append(f"{scale_url} {swidth}w") srcset = ", ".join(srcset_parts) # Merge parameters merged = kwargs.copy() - if "alt" not in merged: - alt = getattr(item, "Title", "") - if callable(alt): - alt = alt() - merged["alt"] = alt - if "width" not in merged: - merged["width"] = data.get("width") + merged["width"] = width if "height" not in merged: - merged["height"] = data.get("height") + merged["height"] = height return self._build_tag(src_url, srcset=srcset, **merged) @@ -89,7 +99,7 @@ def _generate_fixed_tag(self, item, data, **kwargs): scales = data.get("scales", {}) # If a specific scale is requested via scale parameter - scale_name = kwargs.get("scale") + scale_name = kwargs.pop("scale", None) if scale_name and scale_name in scales: scale_info = scales[scale_name] src_url = f"{base_url}/{scale_info['download']}" @@ -103,12 +113,6 @@ def _generate_fixed_tag(self, item, data, **kwargs): # Merge parameters merged = kwargs.copy() - if "alt" not in merged: - alt = getattr(item, "Title", "") - if callable(alt): - alt = alt() - merged["alt"] = alt - if "width" not in merged: merged["width"] = width if "height" not in merged: @@ -122,10 +126,6 @@ def _build_tag(self, src, srcset=None, **kwargs): if srcset: tag += f' srcset="{srcset}"' - # Handle loading default - if "loading" not in kwargs: - tag += ' loading="lazy"' - # Handle class/css_class css_class = kwargs.pop("css_class", None) or kwargs.pop("class", None) if css_class: diff --git a/src/cs/srcset/tests/test_helper.py b/src/cs/srcset/tests/test_helper.py index ae63426..091e662 100644 --- a/src/cs/srcset/tests/test_helper.py +++ b/src/cs/srcset/tests/test_helper.py @@ -53,12 +53,33 @@ def test_news_item_srcset_brain(self): ) # Assertions on generated tag + # By default no alt, no loading self.assertIn('src="http://nohost/plone/news1/@@images/image', tag) self.assertIn('srcset="', tag) self.assertIn('sizes="50vw"', tag) self.assertIn('class="my-news-img"', tag) - self.assertIn('alt="News 1"', tag) - self.assertIn('loading="lazy"', tag) + self.assertNotIn('alt="', tag) + self.assertNotIn('loading="', tag) + # width and height should be present + self.assertIn('width="', tag) + self.assertIn('height="', tag) + + def test_news_item_srcset_with_explicit_attributes(self): + """Test srcset with explicit alt and loading attributes.""" + brains = api.content.find(id="news1") + brain = brains[0] + + tag = self.helper.srcset( + brain, + fieldname="image", + alt="My Alt", + loading="eager", + title="My Title", + ) + + self.assertIn('alt="My Alt"', tag) + self.assertIn('loading="eager"', tag) + self.assertIn('title="My Title"', tag) def test_news_item_srcset_object(self): """Test srcset with a News Item object (fallback path).""" @@ -140,3 +161,18 @@ def test_custom_content_type(self): tag_logo = self.helper.srcset(brain, fieldname="logo", sizes="10vw") self.assertIn('srcset="', tag_logo) self.assertIn("logo", tag_logo) + + def test_srcset_scale_in_src(self): + """Test selecting a specific scale for the src attribute in srcset.""" + brains = api.content.find(id="news1") + brain = brains[0] + + # If 'huge' is available in metadata, it should use it. + # News Item LeadImage behavior usually has 'huge' scale. + tag = self.helper.srcset(brain, fieldname="image", scale_in_src="teaser") + self.assertIn("/@@images/image-", tag) + self.assertIn('src="http://nohost/plone/news1/@@images/image', tag) + # We can check if dimensions match teaser if we knew them, + # but at least check it doesn't crash and has width/height. + self.assertIn('width="', tag) + self.assertIn('height="', tag) From 60c2ae7c7495d7f816d512b4127e469a648965c9 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Tue, 28 Jul 2026 06:44:05 +0200 Subject: [PATCH 08/12] Remove Plone 5.2 and Python 3.7/3.8 support. Add towncrier configuration and breaking change entry. --- .github/workflows/plone-package.yml | 22 +--------------------- buildout.cfg | 3 ++- constraints.txt | 2 +- constraints_plone52.txt | 9 --------- news/2.breaking | 1 + pyproject.toml | 12 ++++++++++++ requirements.txt | 2 +- requirements_plone52.txt | 3 --- setup.py | 10 ++++++---- test_plone52.cfg | 13 ------------- tox.ini | 17 ++--------------- 11 files changed, 26 insertions(+), 68 deletions(-) delete mode 100644 constraints_plone52.txt create mode 100644 news/2.breaking delete mode 100644 requirements_plone52.txt delete mode 100644 test_plone52.cfg diff --git a/.github/workflows/plone-package.yml b/.github/workflows/plone-package.yml index 2a6f7a4..43dfc25 100644 --- a/.github/workflows/plone-package.yml +++ b/.github/workflows/plone-package.yml @@ -16,28 +16,12 @@ jobs: fail-fast: false matrix: plone-version: - - "Plone52" - "Plone60" - "Plone61" - "Plone62" - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] exclude: - - plone-version: "Plone52" - python-version: "3.9" - - plone-version: "Plone52" - python-version: "3.10" - - plone-version: "Plone52" - python-version: "3.11" - - plone-version: "Plone52" - python-version: "3.12" - - plone-version: "Plone52" - python-version: "3.13" - - plone-version: "Plone52" - python-version: "3.14" - - - plone-version: "Plone60" - python-version: "3.8" - plone-version: "Plone60" python-version: "3.12" - plone-version: "Plone60" @@ -45,15 +29,11 @@ jobs: - plone-version: "Plone60" python-version: "3.14" - - plone-version: "Plone61" - python-version: "3.8" - plone-version: "Plone61" python-version: "3.9" - plone-version: "Plone61" python-version: "3.14" - - plone-version: "Plone62" - python-version: "3.8" - plone-version: "Plone62" python-version: "3.9" diff --git a/buildout.cfg b/buildout.cfg index cc9a961..4c0f309 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -7,5 +7,6 @@ extends = # test_plone50.cfg # test_plone51.cfg # test_plone52.cfg - test_plone60.cfg +# test_plone60.cfg # test_plone61.cfg + test_plone62.cfg diff --git a/constraints.txt b/constraints.txt index c1fb3a1..bbad162 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1 +1 @@ --c constraints_plone60.txt +-c constraints_plone62.txt diff --git a/constraints_plone52.txt b/constraints_plone52.txt deleted file mode 100644 index 076f8d4..0000000 --- a/constraints_plone52.txt +++ /dev/null @@ -1,9 +0,0 @@ --c https://dist.plone.org/release/5.2-latest/requirements.txt - -# setuptools==40.2.0 -# zc.buildout==2.13.2 - -isort>=5.12.0 -black==22.8.0 -tox==4.11.3 -flake8==5.0.4 diff --git a/news/2.breaking b/news/2.breaking new file mode 100644 index 0000000..10c3cc5 --- /dev/null +++ b/news/2.breaking @@ -0,0 +1 @@ +Remove Plone 5.2 and Python 3.7/3.8 as unsupported. diff --git a/pyproject.toml b/pyproject.toml index 3c2d546..a8a4f06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,15 @@ [build-system] requires = ["setuptools>=68.2,<=75.8.0"] build-backend = "setuptools.build_meta" + +[tool.towncrier] +directory = "news/" +filename = "CHANGES.rst" +title_format = "{version} (unreleased)" +underlines = ["", "-"] +issue_format = "[#{issue}](https://github.com/codesyntax/cs.srcset/pull/{issue})" +type = [ + { directory = "breaking", name = "Breaking changes", showcontent = true }, + { directory = "feature", name = "New features", showcontent = true }, + { directory = "bugfix", name = "Bug fixes", showcontent = true }, +] diff --git a/requirements.txt b/requirements.txt index 0ba7f38..65c13b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ --r requirements_plone60.txt +-r requirements_plone62.txt diff --git a/requirements_plone52.txt b/requirements_plone52.txt deleted file mode 100644 index fa2f614..0000000 --- a/requirements_plone52.txt +++ /dev/null @@ -1,3 +0,0 @@ --c constraints_plone52.txt -setuptools -zc.buildout diff --git a/setup.py b/setup.py index c37ce16..04a29b9 100644 --- a/setup.py +++ b/setup.py @@ -23,14 +23,16 @@ "Environment :: Web Environment", "Framework :: Plone", "Framework :: Plone :: Addon", - "Framework :: Plone :: 5.2", "Framework :: Plone :: 6.0", + "Framework :: Plone :: 6.1", + "Framework :: Plone :: 6.2", "Programming Language :: Python", - "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", "Operating System :: OS Independent", "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", ], @@ -50,7 +52,7 @@ package_dir={"": "src"}, include_package_data=True, zip_safe=False, - python_requires=">=3.7", + python_requires=">=3.9", install_requires=[ "setuptools", # -*- Extra requirements: -*- diff --git a/test_plone52.cfg b/test_plone52.cfg deleted file mode 100644 index ed1d481..0000000 --- a/test_plone52.cfg +++ /dev/null @@ -1,13 +0,0 @@ -[buildout] - -extends = - https://raw.githubusercontent.com/collective/buildout.plonetest/master/test-5.2.x.cfg - https://raw.githubusercontent.com/collective/buildout.plonetest/master/qa.cfg - base.cfg - -update-versions-file = test_plone52.cfg - -[versions] -plone.testing = 7.0.1 -collective.recipe.vscode = >=0.1.6 -importlib-metadata = 1.1.3 diff --git a/tox.ini b/tox.ini index 0b1e501..b6121de 100644 --- a/tox.ini +++ b/tox.ini @@ -3,13 +3,11 @@ min_version = 4.11.0 envlist = - py38-lint, py39-lint, py310-lint, py311-lint, py313-lint, black-check, - py{38}-Plone{52}, py{39,310,311,312,313}-Plone{60}, py{310,311,312,313}-Plone{61}, py{310,311,312,313,314}-Plone{62}, @@ -21,7 +19,6 @@ skip_missing_interpreters = True [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 @@ -30,9 +27,9 @@ python = 3.14: py314 + [gh-actions:env] PLONE-VERSION = - Plone52: Plone52 Plone60: Plone60 Plone61: Plone61 Plone62: Plone62 @@ -55,14 +52,11 @@ commands = setenv = COVERAGE_FILE=.coverage.{envname} # version_file=test_plone60.cfg - Plone52: version_file=test_plone52.cfg Plone60: version_file=test_plone60.cfg Plone61: version_file=test_plone61.cfg Plone62: version_file=test_plone62.cfg deps = - Plone52: -rrequirements_plone52.txt -# Plone52: -cconstraints_plone52.txt Plone60: -rrequirements_plone60.txt # Plone60: -cconstraints_plone60.txt Plone61: -rrequirements_plone61.txt @@ -78,7 +72,7 @@ basepython = python3 deps = coverage - -cconstraints_plone60.txt + -cconstraints_plone62.txt setenv = COVERAGE_FILE=.coverage @@ -163,13 +157,6 @@ commands = black -v src setup.py -[testenv:py38-lint] -basepython = python3.8 -skip_install = true -deps = {[lint]deps} -commands = {[lint]commands} -allowlist_externals = {[lint]allowlist_externals} - [testenv:py39-lint] basepython = python3.9 skip_install = true From 0e72079267fc9daa6a7eb7767b517d47c7e92588 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Tue, 28 Jul 2026 06:45:24 +0200 Subject: [PATCH 09/12] changelog --- news/2.breaking | 2 +- news/2.feature | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/2.feature diff --git a/news/2.breaking b/news/2.breaking index 10c3cc5..bce5d7c 100644 --- a/news/2.breaking +++ b/news/2.breaking @@ -1 +1 @@ -Remove Plone 5.2 and Python 3.7/3.8 as unsupported. +Remove Plone 5.2 and Python 3.7/3.8 as unsupported. @erral diff --git a/news/2.feature b/news/2.feature new file mode 100644 index 0000000..f8a9598 --- /dev/null +++ b/news/2.feature @@ -0,0 +1 @@ +Add a @@image-helper view to create image URLs from catalog metadata @erral From 2376e2a626b2f1f420e391028b0e978c7f9a83f8 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Tue, 28 Jul 2026 07:21:26 +0200 Subject: [PATCH 10/12] Update QA tools and GHA configuration for better compatibility across Python versions --- .github/workflows/plone-package.yml | 2 -- constraints_plone60.txt | 4 ++-- constraints_plone61.txt | 4 ++-- constraints_plone62.txt | 4 ++-- tox.ini | 3 --- 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/.github/workflows/plone-package.yml b/.github/workflows/plone-package.yml index 43dfc25..4c2072e 100644 --- a/.github/workflows/plone-package.yml +++ b/.github/workflows/plone-package.yml @@ -28,12 +28,10 @@ jobs: python-version: "3.13" - plone-version: "Plone60" python-version: "3.14" - - plone-version: "Plone61" python-version: "3.9" - plone-version: "Plone61" python-version: "3.14" - - plone-version: "Plone62" python-version: "3.9" diff --git a/constraints_plone60.txt b/constraints_plone60.txt index 067b3b9..54e0ec1 100644 --- a/constraints_plone60.txt +++ b/constraints_plone60.txt @@ -12,5 +12,5 @@ #certifi ; platform_system == 'Windows' tox==4.11.3 isort>=5.12.0 -black==22.8.0 -flake8==5.0.4 +black>=24.4.2 +flake8>=7.0.0 diff --git a/constraints_plone61.txt b/constraints_plone61.txt index c4e9731..61920c5 100644 --- a/constraints_plone61.txt +++ b/constraints_plone61.txt @@ -12,5 +12,5 @@ #certifi ; platform_system == 'Windows' tox==4.11.3 isort>=5.12.0 -black==22.8.0 -flake8==5.0.4 +black>=24.4.2 +flake8>=7.0.0 diff --git a/constraints_plone62.txt b/constraints_plone62.txt index 991d419..0d12158 100644 --- a/constraints_plone62.txt +++ b/constraints_plone62.txt @@ -12,5 +12,5 @@ #certifi ; platform_system == 'Windows' tox==4.11.3 isort>=5.12.0 -black==22.8.0 -flake8==5.0.4 +black>=24.4.2 +flake8>=7.0.0 diff --git a/tox.ini b/tox.ini index b6121de..a8b2f70 100644 --- a/tox.ini +++ b/tox.ini @@ -68,7 +68,6 @@ deps = [testenv:coverage-report] skip_install = true usedevelop = True -basepython = python3 deps = coverage @@ -136,7 +135,6 @@ commands = [testenv:black-check] -basepython = python3 skip_install = True deps = -cconstraints.txt @@ -147,7 +145,6 @@ commands = [testenv:black-enforce] -basepython = python3 skip_install = True deps = -cconstraints.txt From f6cc42db0ea346189437da1d74d270a6444cd765 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Tue, 28 Jul 2026 07:22:52 +0200 Subject: [PATCH 11/12] Remove constraints from linting tools and remove obsolete buildout bootstrap command --- tox.ini | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tox.ini b/tox.ini index a8b2f70..aea4601 100644 --- a/tox.ini +++ b/tox.ini @@ -43,8 +43,6 @@ extras = test commands = - {envbindir}/buildout -q -c {toxinidir}/{env:version_file} buildout:directory={envdir} buildout:develop={toxinidir} bootstrap -# {envbindir}/buildout -c {toxinidir}/{env:version_file} buildout:directory={envdir} buildout:develop={toxinidir} annotate {envbindir}/buildout -n -qq -c {toxinidir}/{env:version_file} buildout:directory={envdir} buildout:develop={toxinidir} install test robot coverage run {envbindir}/test -v1 --auto-color {posargs} # coverage run {envbindir}/test -v --all -t robot {posargs} @@ -71,7 +69,6 @@ usedevelop = True deps = coverage - -cconstraints_plone62.txt setenv = COVERAGE_FILE=.coverage @@ -88,7 +85,6 @@ commands = skip_install = true deps = - -cconstraints.txt isort flake8 # helper to generate HTML reports: @@ -127,7 +123,6 @@ allowlist_externals = skip_install = true deps = - -cconstraints.txt isort commands = @@ -137,7 +132,6 @@ commands = [testenv:black-check] skip_install = True deps = - -cconstraints.txt black commands = @@ -147,7 +141,6 @@ commands = [testenv:black-enforce] skip_install = True deps = - -cconstraints.txt black commands = From 0e2ec0b69a36b7b0ba2a0fbba88f4d569be09cb4 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Tue, 28 Jul 2026 07:24:57 +0200 Subject: [PATCH 12/12] Optimize GHA workflow to run tox in a single step and improve env mapping --- .github/workflows/plone-package.yml | 5 +---- tox.ini | 12 ++++++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/plone-package.yml b/.github/workflows/plone-package.yml index 4c2072e..7577f41 100644 --- a/.github/workflows/plone-package.yml +++ b/.github/workflows/plone-package.yml @@ -64,12 +64,9 @@ jobs: run: | python -m pip install --upgrade pip pip install tox tox-gh-actions - - name: Black-Check - run: | - tox -r -e black-check - name: Test with tox run: | - tox -r + tox env: PLONE-VERSION: ${{ matrix.plone-version }} PYTHON-VERSION: ${{ matrix.python-version }} diff --git a/tox.ini b/tox.ini index aea4601..1e38b5c 100644 --- a/tox.ini +++ b/tox.ini @@ -19,12 +19,12 @@ skip_missing_interpreters = True [gh-actions] python = - 3.9: py39 - 3.10: py310 - 3.11: py311 - 3.12: py312 - 3.13: py313 - 3.14: py314 + 3.9: py39, black-check + 3.10: py310, black-check + 3.11: py311, black-check + 3.12: py312, black-check + 3.13: py313, black-check + 3.14: py314, black-check