Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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)
Comment thread
jamadden marked this conversation as resolved.
Outdated
==================

- 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 mpypy to read this file.
Comment thread
jamadden marked this conversation as resolved.
Outdated
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