Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/plone-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ jobs:
- name: Ruff
run: tox -e ruff-check

mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
- name: Install tox
run: python -m pip install tox
- name: Mypy
run: tox -e mypy

test:
runs-on: ubuntu-latest
needs: lint
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Run the tests and the linters with tox:
tox -e py314-Plone62 # one Python and Plone version
tox -e ruff-check # lint and format check
tox -e ruff-fix # fix and format
tox -e mypy # type check src and tests
tox # the whole matrix
```

Expand Down
54 changes: 53 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ release = [
"zest.pocompile",
]

[dependency-groups]
dev = [
"mypy==2.4.0",
]

[project.urls]
Homepage = "https://github.com/cmscom/c2.transform.msoffice"
PyPI = "https://pypi.org/project/c2.transform.msoffice"
Expand Down Expand Up @@ -122,6 +127,8 @@ fix = true
lint.select = [
# flake8-2020
"YTT",
# flake8-annotations
"ANN",
# flake8-bandit
"S",
# flake8-bugbear
Expand Down Expand Up @@ -165,7 +172,52 @@ order-by-type = false
preview = true

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["E501", "RUF001", "S101"]
# Test functions take pytest fixtures and return nothing; fixtures and
# helpers in tests are annotated by convention.
"tests/*" = ["ANN001", "ANN201", "E501", "RUF001", "S101"]

[tool.mypy]
python_version = "3.10"
# Check the sources, not the installed package (src/c2 and src/c2/transform
# are namespace packages).
mypy_path = "src"
explicit_package_bases = true
disallow_untyped_defs = true
check_untyped_defs = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_return_any = true
warn_unused_configs = true

[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_defs = false

# Zope, Plone and their test tools have no type information.
[[tool.mypy.overrides]]
module = [
"Acquisition",
"OFS.Application",
"plone",
"plone.app.contenttypes.content",
"plone.app.contenttypes.indexers",
"plone.app.contenttypes.testing",
"plone.app.testing",
"plone.base.interfaces",
"plone.namedfile.file",
"Products.CMFCore.utils",
"Products.CMFPlone.controlpanel.browser.quickinstaller",
"Products.CMFPlone.Portal",
"Products.GenericSetup.tool",
"Products.PortalTransforms.data",
"Products.PortalTransforms.interfaces",
"Products.PortalTransforms.TransformEngine",
"pytest_plone",
"zope.component",
"zope.configuration.config",
"zope.interface",
]
ignore_missing_imports = true

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
7 changes: 4 additions & 3 deletions src/c2/transform/msoffice/setuphandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from c2.transform.msoffice import logger
from plone.base.interfaces import INonInstallable
from Products.CMFCore.utils import getToolByName
from Products.GenericSetup.tool import SetupTool
from zope.interface import implementer


Expand All @@ -10,12 +11,12 @@

@implementer(INonInstallable)
class HiddenProfiles:
def getNonInstallableProfiles(self):
def getNonInstallableProfiles(self) -> list[str]:
"""Hide the uninstall profile from site creation and the add-ons list."""
return ["c2.transform.msoffice:uninstall"]


def post_install(context):
def post_install(context: SetupTool) -> None:
"""Register the Office MIME types and the transform.

``context`` is the setup tool. Running it again changes nothing.
Expand Down Expand Up @@ -47,7 +48,7 @@ def post_install(context):
transforms.manage_addTransform(config.TRANSFORM_NAME, TRANSFORM_MODULE)


def uninstall(context):
def uninstall(context: SetupTool) -> None:
"""Remove the transform. The MIME types stay; others may use them."""
transforms = getToolByName(context, "portal_transforms")
if config.TRANSFORM_NAME not in transforms.objectIds():
Expand Down
9 changes: 7 additions & 2 deletions src/c2/transform/msoffice/testing.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
from OFS.Application import Application
from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
from plone.app.testing import applyProfile
from plone.app.testing import FunctionalTesting
from plone.app.testing import IntegrationTesting
from plone.app.testing import PloneSandboxLayer
from Products.CMFPlone.Portal import PloneSite
from zope.configuration.config import ConfigurationMachine

import c2.transform.msoffice


class Layer(PloneSandboxLayer):
defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)

def setUpZope(self, app, configurationContext):
def setUpZope(
self, app: Application, configurationContext: ConfigurationMachine
) -> None:
# z3c.autoinclude is disabled in the Plone fixture base layer.
self.loadZCML(package=c2.transform.msoffice)

def setUpPloneSite(self, portal):
def setUpPloneSite(self, portal: PloneSite) -> None:
applyProfile(portal, "c2.transform.msoffice:default")


Expand Down
12 changes: 8 additions & 4 deletions src/c2/transform/msoffice/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from c2.transform.msoffice.config import SITE_CHARSET
from c2.transform.msoffice.config import TRANSFORM_NAME
from pathlib import Path
from Products.PortalTransforms.data import datastream
from Products.PortalTransforms.interfaces import ITransform
from zope.interface import implementer

Expand Down Expand Up @@ -116,16 +117,19 @@ class msoffice_to_text:

output_encoding = SITE_CHARSET

def __init__(self, name=None):
def __init__(self, name: str | None = None) -> None:
if name:
self.__name__ = name

def name(self):
def name(self) -> str:
return self.__name__

def convert(self, orig, data, **kwargs):
def convert(
self, orig: bytes | str, data: datastream, **kwargs: object
) -> datastream:
"""Set the text of ``orig`` (bytes) on ``data``.

``data`` is the datastream that portal_transforms made for the result.
On any failure the text is empty, so saving the file still works.
"""
if not isinstance(orig, bytes):
Expand All @@ -140,5 +144,5 @@ def convert(self, orig, data, **kwargs):
return data


def register():
def register() -> msoffice_to_text:
return msoffice_to_text()
2 changes: 1 addition & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


@pytest.fixture
def stdlib_mimetypes_only(monkeypatch):
def stdlib_mimetypes_only(monkeypatch: pytest.MonkeyPatch) -> None:
"""Forget the host's mime.types (slim images do not have one).

mimetypes.init(files=[]) keeps the current database, so it is replaced.
Expand Down
18 changes: 11 additions & 7 deletions tests/test_java.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

from pathlib import Path
from plone import api
from plone.app.contenttypes.content import File
from plone.app.contenttypes.indexers import SearchableText_file
from plone.namedfile.file import NamedBlobFile
from Products.CMFPlone.Portal import PloneSite

import logging
import os
Expand All @@ -27,21 +29,21 @@


@pytest.fixture
def java():
def java() -> None:
if shutil.which("java") is None:
if os.environ.get(REQUIRE_JAVA) == "1":
pytest.fail(f"java is not on PATH and {REQUIRE_JAVA}=1")
pytest.skip("java is not on PATH")


@pytest.fixture
def no_locale(monkeypatch):
def no_locale(monkeypatch: pytest.MonkeyPatch) -> None:
"""Run java without a locale, like a slim container image."""
monkeypatch.setenv("LC_ALL", "C")
monkeypatch.delenv("LANG", raising=False)


def add_file(portal, name: str, content_type: str):
def add_file(portal: PloneSite, name: str, content_type: str) -> File:
with api.env.adopt_roles(["Manager"]):
return api.content.create(
container=portal,
Expand All @@ -56,15 +58,17 @@ def add_file(portal, name: str, content_type: str):
)


def text_of(portal, name: str, content_type: str) -> str:
return SearchableText_file(add_file(portal, name, content_type))()
def text_of(portal: PloneSite, name: str, content_type: str) -> str:
text: str = SearchableText_file(add_file(portal, name, content_type))()
return text


def transformed(portal, name: str, content_type: str) -> str:
def transformed(portal: PloneSite, name: str, content_type: str) -> str:
data = portal.portal_transforms.convertTo(
"text/plain", (SAMPLES / name).read_bytes(), mimetype=content_type
)
return data.getData()
text: str = data.getData()
return text


@pytest.mark.usefixtures("java", "no_locale")
Expand Down
10 changes: 7 additions & 3 deletions tests/test_setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from Acquisition import aq_base
from c2.transform.msoffice import PACKAGE_NAME
from plone.base.interfaces import INonInstallable
from Products.CMFPlone.controlpanel.browser.quickinstaller import InstallerView
from Products.CMFPlone.Portal import PloneSite
from Products.GenericSetup.tool import SetupTool
from Products.PortalTransforms.TransformEngine import TransformTool
from zope.component import getUtility

import logging
Expand All @@ -14,12 +18,12 @@


@pytest.fixture
def transforms(portal):
def transforms(portal: PloneSite) -> TransformTool:
return portal.portal_transforms


@pytest.fixture
def setup_tool(portal):
def setup_tool(portal: PloneSite) -> SetupTool:
return portal.portal_setup


Expand Down Expand Up @@ -70,7 +74,7 @@ def test_uninstall_profile_is_hidden(portal):

class TestUninstall:
@pytest.fixture(autouse=True)
def uninstalled(self, installer):
def uninstalled(self, installer: InstallerView) -> None:
installer.uninstall_product(PACKAGE_NAME)

def test_addon_uninstalled(self, installer):
Expand Down
32 changes: 24 additions & 8 deletions tests/test_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,22 @@
class FakeRun:
"""Stands in for subprocess.run and records the calls."""

def __init__(self, returncode=0, stdout=b"", stderr=b"", error=None):
def __init__(
self,
returncode: int = 0,
stdout: bytes = b"",
stderr: bytes = b"",
error: Exception | None = None,
) -> None:
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
self.error = error
self.calls = []
self.calls: list[tuple[list[str], dict[str, object]]] = []

def __call__(self, args, **kwargs):
def __call__(
self, args: list[str], **kwargs: object
) -> subprocess.CompletedProcess[bytes]:
self.calls.append((args, kwargs))
if self.error is not None:
raise self.error
Expand All @@ -34,22 +42,30 @@ def __call__(self, args, **kwargs):


@pytest.fixture(autouse=True)
def clean_environment(monkeypatch):
def clean_environment(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("C2_TRANSFORM_MSOFFICE_TIMEOUT", raising=False)
monkeypatch.delenv("C2_TRANSFORM_MSOFFICE_JAVA_OPTS", raising=False)


def fake_run(monkeypatch, **kwargs) -> FakeRun:
run = FakeRun(**kwargs)
def fake_run(
monkeypatch: pytest.MonkeyPatch,
*,
returncode: int = 0,
stdout: bytes = b"",
stderr: bytes = b"",
error: Exception | None = None,
) -> FakeRun:
run = FakeRun(returncode, stdout, stderr, error)
monkeypatch.setattr(transform.subprocess, "run", run)
return run


def convert(orig) -> str:
def convert(orig: bytes | str) -> str:
data = datastream("test")
result = transform.msoffice_to_text().convert(orig, data)
assert result is data
return data.getData()
text: str = data.getData()
return text


def test_implements_itransform():
Expand Down
14 changes: 14 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
[tox]
# dependency_groups (the mypy environment) needs tox 4.22
min_version = 4.22
envlist =
ruff-check,
mypy,
py{310,311,312,313}-Plone60,
py{310,311,312,313}-Plone61,
py{310,311,312,313,314}-Plone62,
Expand Down Expand Up @@ -49,6 +52,17 @@ commands =
ruff format --check --diff src tests


[testenv:mypy]
basepython = python3.14
dependency_groups = dev
setenv =
constraints_file=constraints_plone62.txt
deps =
-rrequirements_plone62.txt
commands =
mypy src tests


[testenv:ruff-fix]
basepython = python3.12
skip_install = True
Expand Down
Loading