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
8 changes: 6 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
=========


3.0.1 (unreleased)
3.1.0 (unreleased)
==================

- Document the ``to_json_representation`` variants and add one
that guarantees sorted keys. Make the "fast" variant not dependent
on second-chance externalization.

- Renamed the "datetime" module to "datetime_ext" to avoid conflicts
with the standard library. Backwards compatibility shims are in place.
- Remove some long-deprecated parameters that were typically
undocumented.
- Introduce some basic type annotations.

3.0.0 (2026-05-07)
==================
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ include tox.ini
include *.txt
include .isort.cfg
include pyproject.toml
include .readthedocs.yml
include .pylintrc
include make-manylinux
exclude .nti_cover_package
Expand Down
2 changes: 1 addition & 1 deletion docs/api/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Datetime
==========

.. automodule:: nti.externalization.datetime
.. automodule:: nti.externalization.datetime_ext
48 changes: 47 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,54 @@ requires = [
# failing in Python 2 (https://travis-ci.org/github/gevent/gevent/jobs/683782800);
# This was fixed in 3.0a5 (https://github.com/cython/cython/issues/3578)
# 3.0a6 fixes an issue cythonizing source on 32-bit platforms
"Cython >= 3.2.1",
"Cython >= 3.2.4",
]

[tool.check-manifest]
ignore = ["*.c"]


[tool.mypy]
# Must be present for mypy to read this file.
follow_imports = "normal"
check_untyped_defs = true
allow_redefinition = true
disable_error_code = [
"method-assign"
]
# Our tests are in terrible shape
# and will need some work to be clean
exclude = [
'test_.*\.py',
]

[[tool.mypy.overrides]]
# third-party untyped code
module = [
"ZODB.*",
"zope.*",
"persistent.*",
"cpuinfo",
"nti.*",
"botocore.*",
"fsspec.*",
"transaction",
"grpc",
"grpc_health.*",
"google.type.*",
"zc.*",
"z3c.*",
"netaddr.*",
"dnslib",
"cytoolz.*",
"toolz.*",
"boto3.*",
"pg8000",
"urllib3.*",
"indexed_gzip",
"pyperf",
"isodate",
"vmprof"

]
ignore_missing_imports = true
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def _c(m):

setup(
name='nti.externalization',
version='3.0.1.dev0',
version='3.1.0.dev0',
author='Jason Madden',
author_email='jason@seecoresoftware.com',
description="NTI Externalization",
Expand Down
5 changes: 5 additions & 0 deletions src/nti/externalization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@
from nti.externalization.representation import to_json_representation
from nti.externalization.internalization import new_from_external_object
from nti.externalization.internalization import update_from_external_object

# BWC hacks
import sys
import nti.externalization.datetime_ext as datetime
sys.modules['nti.externalization.datetime'] = datetime
12 changes: 4 additions & 8 deletions src/nti/externalization/_base_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
This module is **PRIVATE** to this package.

"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import decimal


Expand Down Expand Up @@ -64,7 +60,7 @@ def update_from_other(self, other):
return dict_update(self, other)


def make_external_dict():
def make_external_dict() -> LocatedExternalDict:
# This layer of indirection is for cython; it can't cimport
# types when the extension name doesn't match the
# pxd name. But it can cimport functions that are cpdef to return
Expand Down Expand Up @@ -187,7 +183,7 @@ def get_standard_external_fields():
))


def isSyntheticKey(k):
def isSyntheticKey(k:str) -> bool:
"""
Deprecated. Prefer to test against StandardExternalFields.EXTERNAL_KEYS
"""
Expand Down Expand Up @@ -241,7 +237,7 @@ def __init__(self):

_standard_internal_fields = StandardInternalFields()

def get_standard_internal_fields():
def get_standard_internal_fields() -> StandardInternalFields:
return _standard_internal_fields

# Note that we DO NOT include ``numbers.Number``
Expand Down Expand Up @@ -308,7 +304,7 @@ def __repr__(self): # pragma: no cover
#: The default externalization policy.
DEFAULT_EXTERNALIZATION_POLICY = ExternalizationPolicy()

def get_default_externalization_policy():
def get_default_externalization_policy() -> ExternalizationPolicy:
return DEFAULT_EXTERNALIZATION_POLICY

from nti.externalization._compat import import_c_accel # pylint:disable=wrong-import-position
Expand Down
20 changes: 13 additions & 7 deletions src/nti/externalization/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,24 @@
import os
import sys
import logging
from typing import overload

text_type = str

PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] >= 3
PYPY = hasattr(sys, 'pypy_version_info')
WIN = sys.platform.startswith("win")
LINUX = sys.platform.startswith('linux')
OSX = sys.platform == 'darwin'


PURE_PYTHON = PYPY or os.getenv('PURE_PYTHON') or os.getenv("NTI_EXT_PURE_PYTHON")
PURE_PYTHON = os.getenv('PURE_PYTHON') or os.getenv("NTI_EXT_PURE_PYTHON")


try:
from zope.dublincore.interfaces import IDCTimes # pylint: disable=unused-import
except ModuleNotFoundError:
from zope.interface import Interface
class IDCTimes(Interface): # pylint: disable=inherit-non-class
#pylint: disable-next=inherit-non-class
class IDCTimes(Interface): # type:ignore[no-redef]
"""Mock"""

try:
Expand All @@ -34,7 +33,7 @@ class IDCTimes(Interface): # pylint: disable=inherit-non-class
TRACE = 5
logging.addLevelName(TRACE, "TRACE")

def to_unicode(s, encoding='utf-8', err='strict'):
def to_unicode(s, encoding:str='utf-8', err:str='strict') -> str|None:
"""
Decode a byte sequence and unicode result
"""
Expand All @@ -44,8 +43,15 @@ def to_unicode(s, encoding='utf-8', err='strict'):

text_ = to_unicode

@overload
def bytes_(s:str, encoding:str='', errors:str='') -> bytes:
...

def bytes_(s, encoding='utf-8', errors='strict'):
@overload
def bytes_(s:None, encoding:str='', errors:str='') -> None:
...

def bytes_(s, encoding:str='utf-8', errors:str='strict') -> bytes|None:
"""
If ``s`` is an instance of ``text_type``, return
``s.encode(encoding, errors)``, otherwise return ``s``
Expand Down
12 changes: 4 additions & 8 deletions src/nti/externalization/_interface_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@
A cache based on the interfaces provided by an object.

"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function


from weakref import WeakSet

from zope.interface import providedBy

cache_instances = WeakSet()
cache_instances: WeakSet["InterfaceCache"] = WeakSet()


class InterfaceCache(object):
Expand Down Expand Up @@ -41,7 +36,7 @@ def __init__(self):
self.modified_event_attributes = {}


def cache_for_key_in_providedBy(key, provided_by): # type: (object, object) -> InterfaceCache
def cache_for_key_in_providedBy(key, provided_by) -> InterfaceCache:
# The Declaration objects returned from ``providedBy(obj)`` maintain a _v_attrs that
# gets blown away on changes to themselves or their
# dependents, including adding interfaces dynamically to an instance
Expand All @@ -60,7 +55,7 @@ def cache_for_key_in_providedBy(key, provided_by): # type: (object, object) -> I
return cache


def cache_for(externalizer, ext_self): # type: (object, object) -> InterfaceCache
def cache_for(externalizer, ext_self) -> InterfaceCache:
return cache_for_key_in_providedBy(type(externalizer), providedBy(ext_self))


Expand All @@ -82,4 +77,5 @@ def _cache_cleanUp(instances):

# pylint:disable=wrong-import-position
from nti.externalization._compat import import_c_accel

import_c_accel(globals(), 'nti.externalization.__interface_cache')
21 changes: 11 additions & 10 deletions src/nti/externalization/_threadlocal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,35 @@
Thread local utilities.
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

# stdlib imports
import threading
from collections.abc import Callable
from typing import Generic
from typing import TypeVar

T = TypeVar("T")

# This cannot be optimized (much) with cython, threading.local could be monkey-patched by gevent,
# so this cannot be a cdef class
class ThreadLocalManager(threading.local):
class ThreadLocalManager(threading.local, Generic[T]):

def __init__(self, default):
def __init__(self, default:Callable[[], T]):
# This is called once in each thread, the first time the object
# is used in the thread. The super class does nothing. We use lots
# of threads/greenlets, so save the time.
# pylint:disable=super-init-not-called
self.stack = []
self.stack: list[T] = []
self.default = default

def push(self, info):
def push(self, info:T) -> None:
self.stack.append(info)

set = push # b/c

def pop(self):
def pop(self) -> T|None:
return self.stack.pop() if self.stack else None

def get(self):
def get(self) -> T:
stack = self.stack
if not stack:
return self.default() # Note we're not storing it!
Expand Down
17 changes: 8 additions & 9 deletions src/nti/externalization/autopackage.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,24 @@
"""

import logging
from collections.abc import Iterable

from zope import interface

from zope.dottedname import resolve as dottedname
from zope.mimetype.interfaces import IContentTypeAware

from nti.schema.interfaces import find_most_derived_interface

from ._compat import TRACE
from .datastructures import ModuleScopedInterfaceObjectIO


logger = logging.getLogger(__name__)

# If we extend ExtensionClass.Base, __class_init__ is called automatically
# for each subclass. But we also start participating in acquisition, which
# is probably not what we want
# import ExtensionClass


class _ClassNameRegistry(object):
__name__ = ''

Expand Down Expand Up @@ -74,7 +73,7 @@ def _ap_compute_external_class_name_from_interface_and_instance(cls, unused_ifac
return cls._ap_compute_external_class_name_from_concrete_class(impl.__class__)

@classmethod
def _ap_compute_external_class_name_from_concrete_class(cls, a_type):
def _ap_compute_external_class_name_from_concrete_class(cls, a_type) -> str:
"""
Return the string value of the external class name.

Expand All @@ -87,7 +86,7 @@ def _ap_compute_external_class_name_from_concrete_class(cls, a_type):
return getattr(a_type, '__external_class_name__', a_type.__name__)

@classmethod
def _ap_compute_external_mimetype(cls, package_name, unused_a_type, ext_class_name):
def _ap_compute_external_mimetype(cls, package_name, unused_a_type, ext_class_name) -> str:
"""
Return the string value of the external mime type for the given
type in the given package having the given external name (probably
Expand All @@ -105,7 +104,7 @@ def _ap_compute_external_mimetype(cls, package_name, unused_a_type, ext_class_na

@classmethod
# TODO: We can probably do something with this
def _ap_enumerate_externalizable_root_interfaces(cls, interfaces):
def _ap_enumerate_externalizable_root_interfaces(cls, interfaces) -> Iterable:
"""
Return an iterable of the root interfaces in this package that should be
externalized.
Expand All @@ -115,7 +114,7 @@ def _ap_enumerate_externalizable_root_interfaces(cls, interfaces):
raise NotImplementedError()

@classmethod
def _ap_enumerate_module_names(cls):
def _ap_enumerate_module_names(cls) -> Iterable[str]:
"""
Return an iterable of module names in this package that should be searched to find
factories.
Expand All @@ -125,7 +124,7 @@ def _ap_enumerate_module_names(cls):
raise NotImplementedError()

@classmethod
def _ap_find_potential_factories_in_module(cls, module):
def _ap_find_potential_factories_in_module(cls, module) -> Iterable[type]:
"""
Given a module that we're supposed to examine, iterate over
the types that could be factories.
Expand Down Expand Up @@ -249,7 +248,7 @@ def _ap_handle_one_potential_factory_class(cls, namespace, package_name, impleme
implementation_class.containerId = None

@classmethod
def _ap_find_package_name(cls):
def _ap_find_package_name(cls) -> str:
"""
Return the package name to search for modules.

Expand Down
Loading