Skip to content

Commit 4a36494

Browse files
committed
Add some static typing. This is particularly helpful for subclasses of SchemaConfigured who should now no longer act like Any.
1 parent 41bf2cc commit 4a36494

12 files changed

Lines changed: 117 additions & 78 deletions

File tree

.github/workflows/tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@ jobs:
5656
coverage combine || true
5757
coverage report -i || true
5858
- name: Lint
59-
if: matrix.python-version == '3.12'
59+
if: matrix.python-version == '3.14'
6060
run: |
6161
python -m pip install -U pylint
6262
pylint nti.schema
63+
python -m pip install -U mypy
64+
mypy --install-types --non-interactive src
6365
- name: Submit to Coveralls
6466
uses: coverallsapp/github-action@v2
6567
with:

CHANGES.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
1.20.1 (unreleased)
66
===================
77

8-
- Nothing changed yet.
8+
- Add some minimal typing information. In particular this makes
9+
subclasses of ``SchemaConfigured`` for type-friendly.
910

1011

1112
1.20.0 (2026-06-01)

pyproject.toml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,40 @@ requires = [
33
"wheel",
44
"setuptools>=75.3",
55
]
6+
7+
[tool.mypy]
8+
# Must be present for mpypy to read this file.
9+
follow_imports = "normal"
10+
check_untyped_defs = true
11+
exclude = "tests/*"
12+
#ignore_missing_imports = true
13+
14+
[[tool.mypy.overrides]]
15+
# third-party untyped code
16+
module = [
17+
"Acquisition.*",
18+
"ZODB",
19+
"zope.*",
20+
"persistent.*",
21+
"cpuinfo",
22+
"nti.*",
23+
"botocore.*",
24+
"fsspec.*",
25+
"transaction",
26+
"grpc",
27+
"grpc_health.*",
28+
"google.type.*",
29+
"zc.*",
30+
"z3c.*",
31+
"netaddr.*",
32+
"dnslib",
33+
"cytoolz.*",
34+
"toolz.*",
35+
"boto3.*",
36+
"pg8000",
37+
"urllib3.*",
38+
"indexed_gzip",
39+
"dm.zope.schema.interfaces",
40+
41+
]
42+
ignore_missing_imports = true

src/nti/schema/eqhash.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
"""
77

88
import operator
9+
from collections import abc as abcs
910

1011
__docformat__ = "restructuredtext en"
1112

12-
def _superhash_force(value):
13+
def _superhash_force(value) -> tuple:
1314
# Called when we know that we can't hash the value.
1415
# Dict?
1516
try:
@@ -41,11 +42,14 @@ def _superhash(value):
4142
except TypeError:
4243
return _superhash_force(value)
4344

44-
def EqHash(*names,
45-
**kwargs):
46-
"""
47-
EqHash(*names, include_super=False, superhash=False, supereq=False, include_type=False)
4845

46+
def EqHash[T: type](*names,
47+
include_super=False,
48+
superhash=False,
49+
supereq=False,
50+
include_type=False
51+
) -> abcs.Callable[[T], T]:
52+
"""
4953
A class decorator factory for the common pattern of writing
5054
``__eq__``/``__ne__`` and ``__hash__`` methods that check the same
5155
list of attributes on a given object.
@@ -104,13 +108,9 @@ def EqHash(*names,
104108
comparing mutable and immutable objects (tuple/list).
105109
"""
106110

107-
_include_super = kwargs.pop('include_super', False)
108-
superhash = kwargs.pop("superhash", False)
109-
supereq = kwargs.pop('supereq', False)
110-
_include_type = kwargs.pop('include_type', False)
111+
_include_super = include_super
112+
_include_type = include_type
111113

112-
if kwargs:
113-
raise TypeError("Unexpected keyword args", kwargs)
114114
if not names and not _include_super and not _include_type:
115115
raise TypeError("Asking to hash/eq nothing, but not including super or type")
116116

src/nti/schema/field.py

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,50 +10,45 @@
1010
.. TODO: This module is big enough it should be factored into a package and sub-modules.
1111
"""
1212

13+
import collections.abc as abcs
1314
# stdlib imports
1415
import numbers
1516
import re
17+
from typing import Any
1618

17-
import collections.abc as abcs
18-
19-
19+
import zope.interface.common.idatetime
2020
from zope import interface
2121
from zope import schema
22+
from zope.cachedescriptors.property import Lazy
2223
from zope.deferredimport import deprecatedFrom
23-
2424
from zope.event import notify
25-
import zope.interface.common.idatetime
26-
from zope.cachedescriptors.property import Lazy
27-
28-
from zope.schema import interfaces as sch_interfaces
29-
from zope.schema.interfaces import IFromBytes
30-
from zope.schema.interfaces import IFromUnicode
31-
from zope.schema.interfaces import InvalidValue
32-
3325
from zope.schema import Bool
3426
from zope.schema import Choice
27+
from zope.schema import Complex
3528
from zope.schema import Date
3629
from zope.schema import Datetime
3730
from zope.schema import Decimal
3831
from zope.schema import Dict
3932
from zope.schema import FrozenSet
33+
from zope.schema import Integral
4034
from zope.schema import Iterable
4135
from zope.schema import List
4236
from zope.schema import Mapping
4337
from zope.schema import MutableMapping
4438
from zope.schema import MutableSequence
39+
from zope.schema import Object as _ObjectBase
40+
from zope.schema import Rational
41+
from zope.schema import Real
4542
from zope.schema import Sequence
4643
from zope.schema import Set
4744
from zope.schema import Text
4845
from zope.schema import TextLine
4946
from zope.schema import Timedelta
5047
from zope.schema import Tuple
51-
from zope.schema import Object as _ObjectBase
52-
53-
from zope.schema import Complex
54-
from zope.schema import Real
55-
from zope.schema import Rational
56-
from zope.schema import Integral
48+
from zope.schema import interfaces as sch_interfaces
49+
from zope.schema.interfaces import IFromBytes
50+
from zope.schema.interfaces import IFromUnicode
51+
from zope.schema.interfaces import InvalidValue
5752

5853
from nti.schema import MessageFactory as _
5954
from nti.schema.interfaces import BeforeDictAssignedEvent
@@ -68,7 +63,6 @@
6863
from nti.schema.interfaces import IVariant
6964
from nti.schema.interfaces import VariantValidationError
7065

71-
7266
__docformat__ = "restructuredtext en"
7367

7468
# Re-export some things as part of our public API so we can
@@ -286,7 +280,7 @@ def _fixup_too_long(self, e, value):
286280

287281
def _validate(self, value):
288282
try:
289-
super()._validate(value)
283+
super()._validate(value) # type:ignore[misc]
290284
except sch_interfaces.WrongType as e:
291285
assert e.expected_type is not None, "The expected_type should be provided"
292286
raise
@@ -431,7 +425,7 @@ class Variant(FieldValidationMixin, schema.Field):
431425
would be raised. Now, constructing the variant will raise a ``RequiredMissing``.
432426
"""
433427

434-
fields = ()
428+
fields: abcs.Sequence[schema.Field] = ()
435429

436430
def __init__(self, fields, variant_raise_when_schema_provided=False, **kwargs):
437431
"""
@@ -510,7 +504,7 @@ def _validate(self, value):
510504
raise VariantValidationError(self, value, errors)
511505
finally:
512506
# break cycles
513-
e = errors = None
507+
del errors
514508

515509
def fromObject(self, obj):
516510
"""
@@ -561,7 +555,7 @@ def fromObject(self, obj):
561555
raise VariantValidationError(self, obj, errors)
562556
finally:
563557
# break cycles
564-
ex = errors = None
558+
del errors
565559

566560
_EVENT_TYPES = (
567561
(str, BeforeTextAssignedEvent),
@@ -672,8 +666,9 @@ def fromBytes(self, value):
672666
return self.fromUnicode(value.decode('utf-8'))
673667

674668

675-
_not_stripped = r"^\s|\s$" # space at either beginning or end
676-
_not_stripped = re.compile(_not_stripped).search
669+
_not_stripped = re.compile(
670+
r"^\s|\s$" # space at either beginning or end
671+
).search
677672

678673

679674
def _is_stripped(value):
@@ -764,7 +759,7 @@ class _ValueTypeAddingDocMixin(object):
764759
"""
765760

766761
def getExtraDocLines(self):
767-
lines = super().getExtraDocLines()
762+
lines = super().getExtraDocLines() # type:ignore[misc]
768763
accept_types = getattr(self, 'accept_types', None)
769764
if accept_types:
770765
# Private helper. If it goes away or changes, making sphinx docs will
@@ -799,8 +794,13 @@ class ListOrTuple(IndexedIterable):
799794

800795
@interface.implementer(IFromObject)
801796
class _SequenceFromObjectMixin(object):
802-
accept_types = None
797+
accept_types: abcs.Sequence[type] | None = None
803798
_default_type = list
799+
value_type: schema.Field
800+
_type: type | abcs.Sequence[type] | None
801+
missing_value: Any
802+
required: bool
803+
__name__: str
804804

805805
def __init__(self, *args, **kwargs):
806806
super().__init__(*args, **kwargs)
@@ -844,7 +844,7 @@ def fromObject(self, context):
844844
return context
845845

846846
check_type = self.accept_types or self._type
847-
if check_type is not None and not isinstance(context, check_type):
847+
if check_type is not None and not isinstance(context, check_type): # type:ignore[arg-type]
848848
raise sch_interfaces.WrongType(context, check_type).with_field_and_value(self, context)
849849

850850
result = self._do_fromObject(context)
@@ -864,6 +864,7 @@ def __getattr__(self, name):
864864

865865

866866
class _MapFromObjectMixin(_SequenceFromObjectMixin):
867+
key_type: schema.Field
867868

868869
def __init__(self, *args, **kwargs):
869870
super().__init__(*args, **kwargs)
@@ -891,7 +892,7 @@ def __getattr__(self, name):
891892
return getattr(self.__field, name)
892893

893894

894-
class ListOrTupleFromObject(_SequenceFromObjectMixin, ListOrTuple):
895+
class ListOrTupleFromObject(_SequenceFromObjectMixin, ListOrTuple): # type:ignore[misc]
895896
"""
896897
The ``value_type`` MUST be a :class:`Variant`, or more generally,
897898
something supporting :class:`IFromObject`, :class:`IFromUnicode`

src/nti/schema/fieldproperty.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# stdlib imports
1010
import sys
1111

12-
12+
from zope.interface import Attribute
1313
from zope.schema import interfaces as sch_interfaces
1414
from zope.schema.fieldproperty import FieldProperty
1515
from zope.schema.fieldproperty import FieldPropertyStoredThroughField
@@ -81,7 +81,7 @@ class AdaptingFieldProperty(FieldProperty):
8181
like strings.
8282
"""
8383

84-
def __init__(self, field, name=None):
84+
def __init__(self, field, name=None) -> None:
8585
self.schema = _find_schema_from_field(field)
8686
super().__init__(field, name=name)
8787

@@ -94,14 +94,14 @@ class AdaptingFieldPropertyStoredThroughField(FieldPropertyStoredThroughField):
9494
like strings.
9595
"""
9696

97-
def __init__(self, field, name=None):
97+
def __init__(self, field, name=None) -> None:
9898
self.schema = _find_schema_from_field(field)
9999
super().__init__(field, name=name)
100100

101101
AdaptingFieldPropertyStoredThroughField.__set__ = _make_adapter_set(
102102
AdaptingFieldPropertyStoredThroughField)
103103

104-
def createDirectFieldProperties(__schema, omit=(), adapting=False):
104+
def createDirectFieldProperties(__schema, omit=(), adapting=False) -> None:
105105
"""
106106
Like :func:`zope.schema.fieldproperty.createFieldProperties`, except
107107
only creates properties for fields directly contained within the
@@ -156,7 +156,7 @@ def createDirectFieldProperties(__schema, omit=(), adapting=False):
156156
__frame.f_locals[k] = v
157157

158158

159-
def field_name(field):
159+
def field_name(field: Attribute) -> str:
160160
"""
161161
Produce a clean version of a field's name.
162162

src/nti/schema/interfaces.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,19 @@
66
Also utility functions.
77
"""
88

9-
import warnings
109
import traceback
10+
import warnings
11+
from collections import abc as abcs
1112

1213
from zope.deprecation import deprecated
13-
14-
from zope.schema import Text
15-
from zope.schema import TextLine
16-
from zope.interface import Interface
1714
from zope.interface import Attribute
18-
from zope.interface import providedBy
15+
from zope.interface import Interface
1916
from zope.interface import implementer
17+
from zope.interface import providedBy
18+
from zope.schema import Text
19+
from zope.schema import TextLine
2020
from zope.schema import interfaces as sch_interfaces
21-
try:
22-
from zope.schema._bootstrapfields import BeforeObjectAssignedEvent
23-
except ImportError: # pragma: no cover
24-
# BWC for older zope.schema.
25-
from zope.schema._field import BeforeObjectAssignedEvent
21+
from zope.schema._bootstrapfields import BeforeObjectAssignedEvent
2622

2723
__docformat__ = "restructuredtext en"
2824

@@ -210,9 +206,9 @@ class VariantValidationError(sch_interfaces.ValidationError):
210206
"""
211207

212208
#: A sequence of validation errors
213-
errors = ()
209+
errors: abcs.Sequence[BaseException] = ()
214210

215-
def __init__(self, field, value, errors):
211+
def __init__(self, field, value, errors: abcs.Sequence[BaseException]) -> None:
216212
super().__init__()
217213
self.with_field_and_value(field, value)
218214
self.errors = errors

src/nti/schema/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)