diff --git a/.meta.toml b/.meta.toml index 66cc9f4..435176b 100644 --- a/.meta.toml +++ b/.meta.toml @@ -17,3 +17,14 @@ per-file-ignores = [pyproject] codespell_ignores = "ist," + +[pre-commit] +extra_lines = """ +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.18.2 + hooks: + - id: mypy + additional_dependencies: + - types-decorator + exclude: docs +""" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b63ae5..7cf8940 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -92,3 +92,10 @@ repos: # _your own configuration lines_ # """ ## +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.18.2 + hooks: + - id: mypy + additional_dependencies: + - types-decorator + exclude: docs diff --git a/news/+d8c22f55.internal.rst b/news/+d8c22f55.internal.rst new file mode 100644 index 0000000..ab2e95b --- /dev/null +++ b/news/+d8c22f55.internal.rst @@ -0,0 +1 @@ +Add type hints [@ale-rt] diff --git a/news/+meta.internal b/news/+meta.internal new file mode 100644 index 0000000..c08f539 --- /dev/null +++ b/news/+meta.internal @@ -0,0 +1,2 @@ +Update configuration files. +[plone devs] diff --git a/src/plone/base/batch.py b/src/plone/base/batch.py index ed202d7..72b0a39 100644 --- a/src/plone/base/batch.py +++ b/src/plone/base/batch.py @@ -1,7 +1,12 @@ from plone.batching.batch import QuantumBatch from plone.batching.utils import calculate_pagerange +from typing import Any +from typing import Dict +from typing import Optional +from typing import Union from zope.deprecation import deprecate from ZTUtils import make_query +from ZTUtils.Lazy import LazyMap class Batch(QuantumBatch): @@ -9,15 +14,15 @@ class Batch(QuantumBatch): def __init__( self, - sequence, - size, - start=0, - end=0, - orphan=0, - overlap=0, - pagerange=7, - quantumleap=0, - b_start_str="b_start", + sequence: Union[range, LazyMap], + size: int, + start: int = 0, + end: int = 0, + orphan: int = 0, + overlap: int = 0, + pagerange: int = 7, + quantumleap: int = 0, + b_start_str: str = "b_start", ): super().__init__( sequence, size, start, end, orphan, overlap, pagerange, quantumleap @@ -46,23 +51,25 @@ def __bool__(self): # For Python 2 compatibility: __nonzero__ = __bool__ - def initialize(self, start, end, size): + def initialize(self, start: int, end: int, size: int): super().initialize(start, end, size) self.pagerange, self.pagerangestart, self.pagerangeend = calculate_pagerange( - self.pagenumber, self.numpages, self.pagerange + self.pagenumber, self.numpages, self.pagerange # type: ignore[has-type] ) - def pageurl(self, formvariables, pagenumber=-1): + def pageurl(self, formvariables: Dict[Any, Any], pagenumber: int = -1) -> str: # Makes the url for a given page. if pagenumber == -1: pagenumber = self.pagenumber b_start = pagenumber * (self.pagesize - self.overlap) - self.pagesize return make_query(formvariables, {self.b_start_str: b_start}) - def navurls(self, formvariables, navlist=None): + def navurls( + self, formvariables: Dict[Any, Any], navlist: Optional[range] = None + ) -> map: # Returns the page number and url for the navigation quick links. if navlist is None: - navlist = [] + navlist = range(0) if not navlist: navlist = self.navlist return map( @@ -70,11 +77,11 @@ def navurls(self, formvariables, navlist=None): navlist, ) - def prevurls(self, formvariables): + def prevurls(self, formvariables: Dict[Any, Any]) -> map: # Helper method to get prev navigation list from templates. return self.navurls(formvariables, self.previous_pages) - def nexturls(self, formvariables): + def nexturls(self, formvariables: Dict[Any, Any]) -> map: # Helper method to get next navigation list from templates. return self.navurls(formvariables, self.next_pages) diff --git a/src/plone/base/i18nl10n.py b/src/plone/base/i18nl10n.py index 849fe93..bbd2445 100644 --- a/src/plone/base/i18nl10n.py +++ b/src/plone/base/i18nl10n.py @@ -3,12 +3,18 @@ """ from Acquisition import aq_acquire +from datetime import datetime from DateTime import DateTime from DateTime.interfaces import IDateTime +from plone.base.tests.test_i18nl10n import DummyContext from plone.registry.interfaces import IRegistry +from typing import cast +from typing import Optional +from typing import Union from zope.component import getUtility from zope.i18n import translate from zope.i18n.locales import locales +from zope.publisher.browser import TestRequest from zope.publisher.interfaces.browser import IBrowserRequest import logging @@ -128,8 +134,13 @@ def setDefaultTimeFormat(localeid, value): def utranslate( - domain, msgid, mapping=None, context=None, target_language=None, default=None -): + domain: str, + msgid: str, + mapping: None = None, + context: Optional[TestRequest] = None, + target_language: Optional[str] = None, + default: Optional[str] = None, +) -> str: # We used to pass an object as context. if not IBrowserRequest.providedBy(context): context = aq_acquire(context, "REQUEST") @@ -156,14 +167,14 @@ def get_formatstring_from_registry(msgid): def ulocalized_time( - time, - long_format=None, - time_only=False, - context=None, - domain="plonelocales", - request=None, - target_language=None, -): + time: Optional[Union[datetime, str, int]], + long_format: Optional[Union[str, bool]] = None, + time_only: Optional[bool] = False, + context: Optional[DummyContext] = None, + domain: Optional[str] = "plonelocales", + request: None = None, + target_language: Optional[str] = None, +) -> Optional[str]: """unicode aware localized time method (l10n)""" if time_only: @@ -208,12 +219,12 @@ def ulocalized_time( try: time = DateTime(time) except Exception: - logger.debug(f"Failed to convert {time} to a DateTime object") + logger.debug("Failed to convert %s to a DateTime object", time) return None if context is None: # when without context, we cannot do very much. - return time.ISO8601() + return cast(DateTime, time).ISO8601() if request is None: request = aq_acquire(context, "REQUEST") @@ -233,7 +244,7 @@ def ulocalized_time( # e.g. %Y instead of ${Y} for the year. This means we cannot do further # i18n/l10n for translating week days or month names. Python will do # translation using the current locale. - return time.strftime(formatstring) + return cast(DateTime, time).strftime(formatstring) else: # 2. the normal case: translation machinery, # that is the ".../LC_MESSAGES/plonelocales.po" files @@ -251,7 +262,7 @@ def ulocalized_time( formatstring = "%H:%M" # 03:14 else: formatstring = "[INTERNAL ERROR]" - return time.strftime(formatstring) + return cast(DateTime, time).strftime(formatstring) # get the format elements used in the formatstring formatelements = {el[2:-1] for el in _interp_regex.findall(formatstring)} @@ -259,18 +270,18 @@ def ulocalized_time( # add used elements to mapping elements = formatelements & datetime_formatvariables for key in elements: - mapping[key] = time.strftime("%" + key) + mapping[key] = cast(DateTime, time).strftime("%" + key) # add weekday name, abbr. weekday name, month name, abbr month name name_elements = formatelements & name_formatvariables if {"a", "A"} & name_elements: - weekday = int(time.strftime("%w")) # weekday, sunday = 0 + weekday = int(cast(DateTime, time).strftime("%w")) # weekday, sunday = 0 if "a" in name_elements: mapping["a"] = weekdayname_msgid_abbr(weekday) if "A" in name_elements: mapping["A"] = weekdayname_msgid(weekday) if {"b", "B"} & name_elements: - monthday = int(time.strftime("%m")) # month, january = 1 + monthday = int(cast(DateTime, time).strftime("%m")) # month, january = 1 if "b" in name_elements: mapping["b"] = monthname_msgid_abbr(monthday) if "B" in name_elements: @@ -292,7 +303,9 @@ def ulocalized_time( ) -def _numbertoenglishname(number, format=None, attr="_days"): +def _numbertoenglishname( + number: int, format: Optional[str] = None, attr: str = "_days" +) -> str: # returns the english name of day or month number # starting with Sunday == 0 # and January = 1 @@ -307,38 +320,38 @@ def _numbertoenglishname(number, format=None, attr="_days"): return ENGLISH_NAMES[attr][number] -def monthname_english(number, format=None): +def monthname_english(number: int, format: Optional[str] = None) -> str: # returns the english name of month with number return _numbertoenglishname(number, format=format, attr="_months") -def weekdayname_english(number, format=None): +def weekdayname_english(number: int, format: Optional[str] = None) -> str: # returns the english name of week with number return _numbertoenglishname(number, format=format, attr="_days") -def monthname_msgid(number): +def monthname_msgid(number: int) -> str: # returns the msgid for monthname # use to translate to full monthname (January, February, ...) # e.g. month_jan, month_feb, ... return "month_%s" % monthname_english(number, format="a").lower() -def monthname_msgid_abbr(number): +def monthname_msgid_abbr(number: int) -> str: # returns the msgid for the abbreviated monthname # use to translate to abbreviated format (Jan, Feb, ...) # e.g. month_jan_abbr, month_feb_abbr, ... return "month_%s_abbr" % monthname_english(number, format="a").lower() -def weekdayname_msgid(number): +def weekdayname_msgid(number: int) -> str: # returns the msgid for the weekdayname # use to translate to full weekdayname (Monday, Tuesday, ...) # e.g. weekday_mon, weekday_tue, ... return "weekday_%s" % weekdayname_english(number, format="a").lower() -def weekdayname_msgid_abbr(number): +def weekdayname_msgid_abbr(number: int) -> str: # returns the msgid for abbreviated weekdayname # use to translate to abbreviated format (Mon, Tue, ...) # e.g. weekday_mon_abbr, weekday_tue_abbr, ... diff --git a/src/plone/base/interfaces/constrains.py b/src/plone/base/interfaces/constrains.py index 3686052..e1e11e8 100644 --- a/src/plone/base/interfaces/constrains.py +++ b/src/plone/base/interfaces/constrains.py @@ -1,3 +1,4 @@ +# mypy: disable-error-code=misc from zope.interface import Interface diff --git a/src/plone/base/interfaces/controlpanel.py b/src/plone/base/interfaces/controlpanel.py index a4dfa32..c79e7f4 100644 --- a/src/plone/base/interfaces/controlpanel.py +++ b/src/plone/base/interfaces/controlpanel.py @@ -2,6 +2,7 @@ from plone import schema from plone.base import PloneMessageFactory as _ from Products.CMFCore.utils import getToolByName +from typing import Dict from zope.component.hooks import getSite from zope.interface import Attribute from zope.interface import implementer @@ -15,7 +16,7 @@ import json -def dump_json_to_text(obj): +def dump_json_to_text(obj: Dict[str, Dict[str, str]]) -> str: """Encode an obj into a text""" text = json.dumps(obj, indent=4) if not isinstance(text, str): @@ -59,7 +60,7 @@ def dump_json_to_text(obj): """ -def validate_json(value): +def validate_json(value: str): try: json.loads(value) except ValueError as exc: @@ -100,10 +101,10 @@ def unregisterConfiglet(id): def unregisterApplication(appId): """unregister Application with all configlets""" - def getGroupIds(): + def getGroupIds(): # type: ignore[misc] """list of the group ids""" - def getGroups(): + def getGroups(): # type: ignore[misc] """list of groups as dicts with id and title""" def enumConfiglets(group=None): @@ -1567,7 +1568,7 @@ class IUserGroupsSettingsSchema(Interface): ) -def validate_twitter_username(value): +def validate_twitter_username(value: str): if value and value.startswith("@"): raise Invalid('Twitter username should not include the "@" prefix character.') return True @@ -1959,7 +1960,7 @@ class IPloneControlPanelView(Interface): class IPloneControlPanelForm(IPloneControlPanelView): """Forms using plone.app.controlpanel""" - def _on_save(): + def _on_save(): # type: ignore[misc] """Callback method which can be implemented by control panels to react when the form is successfully saved. This avoids the need to re-define actions only to do some additional notification or diff --git a/src/plone/base/interfaces/defaultpage.py b/src/plone/base/interfaces/defaultpage.py index 296c4b4..59d1cc8 100644 --- a/src/plone/base/interfaces/defaultpage.py +++ b/src/plone/base/interfaces/defaultpage.py @@ -1,3 +1,4 @@ +# mypy: disable-error-code=misc from zope.interface import Interface diff --git a/src/plone/base/interfaces/images.py b/src/plone/base/interfaces/images.py index e2264f9..270d1cd 100644 --- a/src/plone/base/interfaces/images.py +++ b/src/plone/base/interfaces/images.py @@ -1,3 +1,4 @@ +# mypy: disable-error-code=misc from zope.interface import Interface diff --git a/src/plone/base/interfaces/installable.py b/src/plone/base/interfaces/installable.py index 099aeea..f85d64b 100644 --- a/src/plone/base/interfaces/installable.py +++ b/src/plone/base/interfaces/installable.py @@ -1,3 +1,4 @@ +# mypy: disable-error-code=misc from zope.interface import Interface diff --git a/src/plone/base/interfaces/migration.py b/src/plone/base/interfaces/migration.py index 2ae6a85..b026302 100644 --- a/src/plone/base/interfaces/migration.py +++ b/src/plone/base/interfaces/migration.py @@ -1,3 +1,4 @@ +# mypy: disable-error-code=misc from zope.interface import Interface diff --git a/src/plone/base/interfaces/password_reset.py b/src/plone/base/interfaces/password_reset.py index 685b774..803ca8e 100644 --- a/src/plone/base/interfaces/password_reset.py +++ b/src/plone/base/interfaces/password_reset.py @@ -1,3 +1,4 @@ +# mypy: disable-error-code=misc from zope.interface import Attribute from zope.interface import Interface diff --git a/src/plone/base/interfaces/properties.py b/src/plone/base/interfaces/properties.py index 56dd735..338c68b 100644 --- a/src/plone/base/interfaces/properties.py +++ b/src/plone/base/interfaces/properties.py @@ -1,3 +1,4 @@ +# mypy: disable-error-code=misc from zope.interface import Attribute from zope.interface import Interface diff --git a/src/plone/base/interfaces/syndication.py b/src/plone/base/interfaces/syndication.py index fc8f6d6..b68271d 100644 --- a/src/plone/base/interfaces/syndication.py +++ b/src/plone/base/interfaces/syndication.py @@ -1,3 +1,4 @@ +# mypy: disable-error-code=misc from .. import PloneMessageFactory as _ from zope import schema from zope.interface import Interface diff --git a/src/plone/base/navigationroot.py b/src/plone/base/navigationroot.py index bf7564a..b5f64a3 100644 --- a/src/plone/base/navigationroot.py +++ b/src/plone/base/navigationroot.py @@ -1,14 +1,17 @@ from Acquisition import aq_base from Acquisition import aq_inner from Acquisition import aq_parent +from Acquisition import ImplicitAcquisitionWrapper from plone.base.interfaces import INavigationRoot from plone.registry.interfaces import IRegistry from Products.CMFCore.utils import getToolByName +from typing import cast +from typing import Optional from zope.component import getUtility from zope.component.hooks import getSite -def get_navigation_root(context, relativeRoot=None): +def get_navigation_root(context: ImplicitAcquisitionWrapper, relativeRoot=None) -> str: """Get the path to the root of the navigation tree. If a relativeRoot argument is provided, navigation root is computed from @@ -51,10 +54,12 @@ def get_navigation_root(context, relativeRoot=None): # compute the root portal = portal_url.getPortalObject() root = get_navigation_root_object(context, portal) - return "/".join(root.getPhysicalPath()) + return "/".join(cast(ImplicitAcquisitionWrapper, root).getPhysicalPath()) -def get_navigation_root_object(context, portal): +def get_navigation_root_object( + context: None, portal: ImplicitAcquisitionWrapper +) -> Optional[ImplicitAcquisitionWrapper]: obj = context while not INavigationRoot.providedBy(obj) and aq_base(obj) is not aq_base(portal): parent = aq_parent(aq_inner(obj)) diff --git a/src/plone/base/tests/test_doctests.py b/src/plone/base/tests/test_doctests.py index 8d9f2ea..6d9d653 100644 --- a/src/plone/base/tests/test_doctests.py +++ b/src/plone/base/tests/test_doctests.py @@ -1,9 +1,10 @@ from unittest import TestSuite import doctest +import unittest.suite -def test_suite(): +def test_suite() -> unittest.suite.TestSuite: suites = ( doctest.DocFileSuite( "messages.rst", diff --git a/src/plone/base/tests/test_i18nl10n.py b/src/plone/base/tests/test_i18nl10n.py index 4facd0f..04d6e70 100644 --- a/src/plone/base/tests/test_i18nl10n.py +++ b/src/plone/base/tests/test_i18nl10n.py @@ -1,6 +1,9 @@ """Unit tests for plone.base.i18nl10n module.""" from contextlib import contextmanager +from typing import Iterator +from typing import Optional +from typing import Union from unittest.mock import patch from zope.publisher.browser import TestRequest @@ -24,7 +27,7 @@ def __init__(self): @contextmanager -def patch_formatstring(value=None): +def patch_formatstring(value: Optional[str] = None) -> Iterator[None]: import plone.base.i18nl10n with patch.object( @@ -58,12 +61,12 @@ def patch_formatstring(value=None): } -def mock_translate(msgid, *args, **kwargs): +def mock_translate(msgid: str, *args, **kwargs) -> str: from zope.i18n import translate target_language = kwargs.get("target_language") default = kwargs.get("default") - override = False + override = "" # Note: we have translations for target_language=None . try: override = TRANSLATIONS[target_language][msgid] @@ -84,7 +87,7 @@ def mock_translate(msgid, *args, **kwargs): @contextmanager -def patch_translate(): +def patch_translate() -> Iterator[None]: import plone.base.i18nl10n with patch.object(plone.base.i18nl10n, "translate", wraps=mock_translate): @@ -92,13 +95,14 @@ def patch_translate(): @contextmanager -def use_locale(value=None): +def use_locale(value: Optional[str] = None) -> Iterator[Union[str, bool]]: orig = locale.getlocale(locale.LC_TIME)[0] or "C" try: worked = locale.setlocale(locale.LC_TIME, value) except locale.Error: - worked = False - yield worked + yield False + else: + yield worked locale.setlocale(locale.LC_TIME, orig) diff --git a/src/plone/base/utils.py b/src/plone/base/utils.py index 8a79d27..e9157bb 100644 --- a/src/plone/base/utils.py +++ b/src/plone/base/utils.py @@ -8,6 +8,9 @@ from plone.registry.interfaces import IRegistry from Products.CMFCore.interfaces import ITypesTool from Products.CMFCore.utils import getToolByName +from typing import Any +from typing import Optional +from typing import Union from urllib.parse import urlparse from zExceptions import NotFound from ZODB.POSException import ConflictError @@ -34,10 +37,10 @@ } SIZE_ORDER = ("PB", "TB", "GB", "MB", "KB") -_marker = dict() +_marker = object() -def human_readable_size(size): +def human_readable_size(size: Any) -> Any: """Get a human readable size string.""" smaller = SIZE_ORDER[-1] @@ -126,14 +129,14 @@ def safe_text(value, encoding="utf-8") -> str: return value -def safe_bytes(value, encoding="utf-8") -> bytes: +def safe_bytes(value: Any, encoding: str = "utf-8") -> Any: """Convert text to bytes of the specified encoding.""" if isinstance(value, str): value = value.encode(encoding) return value -def safe_hasattr(obj, name, _marker=object()): +def safe_hasattr(obj, name, _marker: Any = _marker): """Make sure we don't mask exceptions like hasattr(). We don't want exceptions other than AttributeError to be masked, @@ -595,7 +598,7 @@ def unrestricted_construct_instance(type_name, container, id, *args, **kw): return fti._constructInstance(container, id, *args, **kw) -def is_truthy(value) -> bool: +def is_truthy(value: Optional[Union[bool, str, int]]) -> bool: """Return `True`, if "yes" was meant, `False` otherwise.""" return bool(value) and str(value).lower() in { "1",