Repository: agronholm/cbor2 (language: python)

Issue:
CBORDecodeEOF comes from module _cbor2 instead of cbor2.types
version: 5.4.6
python: 3.11.3

```python
            eof = False
            while not eof:
                try:
                    item = cbor.load(fp)
                except _cbor2.CBORDecodeEOF:
                    eof = True
```

https://cbor2.readthedocs.io/en/latest/modules/types.html


Discussion / hints:

Fix patch:
diff --git a/cbor2/__init__.py b/cbor2/__init__.py
index 09caa8a8..c8770fd7 100644
--- a/cbor2/__init__.py
+++ b/cbor2/__init__.py
@@ -1,27 +1,36 @@
-from .decoder import CBORDecoder as CBORDecoder
-from .decoder import load as load
-from .decoder import loads as loads
-from .encoder import CBOREncoder as CBOREncoder
-from .encoder import dump as dump
-from .encoder import dumps as dumps
-from .encoder import shareable_encoder as shareable_encoder
-from .types import CBORDecodeEOF as CBORDecodeEOF
-from .types import CBORDecodeError as CBORDecodeError
-from .types import CBORDecodeValueError as CBORDecodeValueError
-from .types import CBOREncodeError as CBOREncodeError
-from .types import CBOREncodeTypeError as CBOREncodeTypeError
-from .types import CBOREncodeValueError as CBOREncodeValueError
-from .types import CBORError as CBORError
-from .types import CBORSimpleValue as CBORSimpleValue
-from .types import CBORTag as CBORTag
-from .types import undefined as undefined
+from typing import Any
+
+from ._decoder import CBORDecoder as CBORDecoder
+from ._decoder import load as load
+from ._decoder import loads as loads
+from ._encoder import CBOREncoder as CBOREncoder
+from ._encoder import dump as dump
+from ._encoder import dumps as dumps
+from ._encoder import shareable_encoder as shareable_encoder
+from ._types import CBORDecodeEOF as CBORDecodeEOF
+from ._types import CBORDecodeError as CBORDecodeError
+from ._types import CBORDecodeValueError as CBORDecodeValueError
+from ._types import CBOREncodeError as CBOREncodeError
+from ._types import CBOREncodeTypeError as CBOREncodeTypeError
+from ._types import CBOREncodeValueError as CBOREncodeValueError
+from ._types import CBORError as CBORError
+from ._types import CBORSimpleValue as CBORSimpleValue
+from ._types import CBORTag as CBORTag
+from ._types import FrozenDict as FrozenDict
+from ._types import undefined as undefined
 
 try:
     from _cbor2 import *  # noqa: F403
 except ImportError:
     # Couldn't import the optimized C version; ignore the failure and leave the
     # pure Python implementations in place.
-    pass
+
+    # Re-export imports so they look like they live directly in this package
+    key: str
+    value: Any
+    for key, value in list(locals().items()):
+        if callable(value) and getattr(value, "__module__", "").startswith("cbor2."):
+            value.__module__ = __name__
 else:
     # The pure Python implementations are replaced with the optimized C
     # variants, but we still need to create the encoder dictionaries for the C
@@ -32,8 +41,8 @@ def _init_cbor2() -> None:
 
         import _cbor2
 
-        from .encoder import canonical_encoders, default_encoders
-        from .types import CBORSimpleValue, CBORTag, undefined  # noqa: F811
+        from ._encoder import canonical_encoders, default_encoders
+        from ._types import CBORSimpleValue, CBORTag, undefined  # noqa: F811
 
         _cbor2.default_encoders = OrderedDict(
             [
diff --git a/cbor2/_decoder.py b/cbor2/_decoder.py
new file mode 100644
index 00000000..194d8372
--- /dev/null
+++ b/cbor2/_decoder.py
@@ -0,0 +1,792 @@
+from __future__ import annotations
+
+import re
+import struct
+import sys
+from collections.abc import Callable, Mapping, Sequence
+from datetime import datetime, timedelta, timezone
+from io import BytesIO
+from typing import TYPE_CHECKING, Any, TypeVar, cast, overload
+
+from ._types import (
+    CBORDecodeEOF,
+    CBORDecodeValueError,
+    CBORSimpleValue,
+    CBORTag,
+    FrozenDict,
+    break_marker,
+    undefined,
+)
+
+if TYPE_CHECKING:
+    from decimal import Decimal
+    from email.message import Message
+    from fractions import Fraction
+    from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
+    from typing import Literal
+    from uuid import UUID
+
+T = TypeVar("T")
+
+timestamp_re = re.compile(
+    r"^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)"
+    r"(?:\.(\d{1,6})\d*)?(?:Z|([+-])(\d\d):(\d\d))$"
+)
+
+
+class CBORDecoder:
+    """
+    The CBORDecoder class implements a fully featured `CBOR`_ decoder with
+    several extensions for handling shared references, big integers, rational
+    numbers and so on. Typically the class is not used directly, but the
+    :func:`load` and :func:`loads` functions are called to indirectly construct
+    and use the class.
+
+    When the class is constructed manually, the main entry points are
+    :meth:`decode` and :meth:`decode_from_bytes`.
+
+    .. _CBOR: https://cbor.io/
+    """
+
+    __slots__ = (
+        "_tag_hook",
+        "_object_hook",
+        "_share_index",
+        "_shareables",
+        "_fp",
+        "_fp_read",
+        "_immutable",
+        "_str_errors",
+        "_stringref_namespace",
+    )
+
+    _fp: BytesIO
+    _fp_read: Callable[[int], bytes]
+
+    def __init__(
+        self,
+        fp: BytesIO,
+        tag_hook: Callable[[CBORDecoder, CBORTag], Any] | None = None,
+        object_hook: Callable[[CBORDecoder, dict[Any, Any]], Any] | None = None,
+        str_errors: Literal["strict", "error", "replace"] = "strict",
+    ):
+        """
+        :param fp:
+            the file to read from (any file-like object opened for reading in binary
+            mode)
+        :param tag_hook:
+            callable that takes 2 arguments: the decoder instance, and the
+            :class:`.CBORTag` to be decoded. This callback is invoked for any tags
+            for which there is no built-in decoder. The return value is substituted
+            for the :class:`.CBORTag` object in the deserialized output
+        :param object_hook:
+            callable that takes 2 arguments: the decoder instance, and a
+            dictionary. This callback is invoked for each deserialized
+            :class:`dict` object. The return value is substituted for the dict in
+            the deserialized output.
+        :param str_errors:
+            determines how to handle unicode decoding errors (see the `Error Handlers`_
+            section in the standard library documentation for details)
+
+        .. _Error Handlers: https://docs.python.org/3/library/codecs.html#error-handlers
+
+        """
+        self.fp = fp
+        self.tag_hook = tag_hook
+        self.object_hook = object_hook
+        self.str_errors = str_errors
+        self._share_index: int | None = None
+        self._shareables: list[object] = []
+        self._stringref_namespace: list[str | bytes] | None = None
+        self._immutable = False
+
+    @property
+    def immutable(self) -> bool:
+        """
+        Used by decoders to check if the calling context requires an immutable
+        type.  Object_hook or tag_hook should raise an exception if this flag
+        is set unless the result can be safely used as a dict key.
+        """
+        return self._immutable
+
+    @property
+    def fp(self) -> BytesIO:
+        return self._fp
+
+    @fp.setter
+    def fp(self, value: BytesIO) -> None:
+        try:
+            if not callable(value.read):
+                raise ValueError("fp.read is not callable")
+        except AttributeError:
+            raise ValueError("fp object has no read method")
+        else:
+            self._fp = value
+            self._fp_read = value.read
+
+    @property
+    def tag_hook(self) -> Callable[[CBORDecoder, CBORTag], Any] | None:
+        return self._tag_hook
+
+    @tag_hook.setter
+    def tag_hook(self, value: Callable[[CBORDecoder, CBORTag], Any] | None) -> None:
+        if value is None or callable(value):
+            self._tag_hook = value
+        else:
+            raise ValueError("tag_hook must be None or a callable")
+
+    @property
+    def object_hook(self) -> Callable[[CBORDecoder, dict[Any, Any]], Any] | None:
+        return self._object_hook
+
+    @object_hook.setter
+    def object_hook(
+        self, value: Callable[[CBORDecoder, Mapping[Any, Any]], Any] | None
+    ) -> None:
+        if value is None or callable(value):
+            self._object_hook = value
+        else:
+            raise ValueError("object_hook must be None or a callable")
+
+    @property
+    def str_errors(self) -> Literal["strict", "error", "replace"]:
+        return self._str_errors
+
+    @str_errors.setter
+    def str_errors(self, value: Literal["strict", "error", "replace"]) -> None:
+        if value in ("strict", "error", "replace"):
+            self._str_errors = value
+        else:
+            raise ValueError(
+                f"invalid str_errors value {value!r} (must be one of 'strict', "
+                "'error', or 'replace')"
+            )
+
+    def set_shareable(self, value: T) -> T:
+        """
+        Set the shareable value for the last encountered shared value marker,
+        if any. If the current shared index is ``None``, nothing is done.
+
+        :param value: the shared value
+        :returns: the shared value to permit chaining
+        """
+        if self._share_index is not None:
+            self._shareables[self._share_index] = value
+
+        return value
+
+    def _stringref_namespace_add(self, string: str | bytes, length: int) -> None:
+        if self._stringref_namespace is not None:
+            next_index = len(self._stringref_namespace)
+            if next_index < 24:
+                is_referenced = length >= 3
+            elif next_index < 256:
+                is_referenced = length >= 4
+            elif next_index < 65536:
+                is_referenced = length >= 5
+            elif next_index < 4294967296:
+                is_referenced = length >= 7
+            else:
+                is_referenced = length >= 11
+
+            if is_referenced:
+                self._stringref_namespace.append(string)
+
+    def read(self, amount: int) -> bytes:
+        """
+        Read bytes from the data stream.
+
+        :param int amount: the number of bytes to read
+        """
+        data = self._fp_read(amount)
+        if len(data) < amount:
+            raise CBORDecodeEOF(
+                f"premature end of stream (expected to read {amount} bytes, got {len(data)} "
+                "instead)"
+            )
+
+        return data
+
+    def _decode(self, immutable: bool = False, unshared: bool = False) -> Any:
+        if immutable:
+            old_immutable = self._immutable
+            self._immutable = True
+        if unshared:
+            old_index = self._share_index
+            self._share_index = None
+        try:
+            initial_byte = self.read(1)[0]
+            major_type = initial_byte >> 5
+            subtype = initial_byte & 31
+            decoder = major_decoders[major_type]
+            return decoder(self, subtype)
+        finally:
+            if immutable:
+                self._immutable = old_immutable
+            if unshared:
+                self._share_index = old_index
+
+    def decode(self) -> object:
+        """
+        Decode the next value from the stream.
+
+        :raises CBORDecodeError: if there is any problem decoding the stream
+        """
+        return self._decode()
+
+    def decode_from_bytes(self, buf: bytes) -> object:
+        """
+        Wrap the given bytestring as a file and call :meth:`decode` with it as
+        the argument.
+
+        This method was intended to be used from the ``tag_hook`` hook when an
+        object needs to be decoded separately from the rest but while still
+        taking advantage of the shared value registry.
+        """
+        with BytesIO(buf) as fp:
+            old_fp = self.fp
+            self.fp = fp
+            retval = self._decode()
+            self.fp = old_fp
+            return retval
+
+    @overload
+    def _decode_length(self, subtype: int) -> int:
+        ...
+
+    @overload
+    def _decode_length(
+        self, subtype: int, allow_indefinite: Literal[True]
+    ) -> int | None:
+        ...
+
+    def _decode_length(
+        self, subtype: int, allow_indefinite: bool = False
+    ) -> int | None:
+        if subtype < 24:
+            return subtype
+        elif subtype == 24:
+            return self.read(1)[0]
+        elif subtype == 25:
+            return cast(int, struct.unpack(">H", self.read(2))[0])
+        elif subtype == 26:
+            return cast(int, struct.unpack(">L", self.read(4))[0])
+        elif subtype == 27:
+            return cast(int, struct.unpack(">Q", self.read(8))[0])
+        elif subtype == 31 and allow_indefinite:
+            return None
+        else:
+            raise CBORDecodeValueError(
+                "unknown unsigned integer subtype 0x%x" % subtype
+            )
+
+    def decode_uint(self, subtype: int) -> int:
+        # Major tag 0
+        return self.set_shareable(self._decode_length(subtype))
+
+    def decode_negint(self, subtype: int) -> int:
+        # Major tag 1
+        return self.set_shareable(-self._decode_length(subtype) - 1)
+
+    def decode_bytestring(self, subtype: int) -> bytes:
+        # Major tag 2
+        length = self._decode_length(subtype, allow_indefinite=True)
+        if length is None:
+            # Indefinite length
+            buf: list[bytes] = []
+            while True:
+                initial_byte = self.read(1)[0]
+                if initial_byte == 0xFF:
+                    result = b"".join(buf)
+                    break
+                elif initial_byte >> 5 == 2:
+                    length = self._decode_length(initial_byte & 0x1F)
+                    if length is None or length > sys.maxsize:
+                        raise CBORDecodeValueError(
+                            "invalid length for indefinite bytestring chunk 0x%x"
+                            % length
+                        )
+                    value = self.read(length)
+                    buf.append(value)
+                else:
+                    raise CBORDecodeValueError(
+                        "non-bytestring found in indefinite length bytestring"
+                    )
+        else:
+            if length > sys.maxsize:
+                raise CBORDecodeValueError(
+                    "invalid length for bytestring 0x%x" % length
+                )
+
+            result = self.read(length)
+            self._stringref_namespace_add(result, length)
+
+        return self.set_shareable(result)
+
+    def decode_string(self, subtype: int) -> str:
+        # Major tag 3
+        length = self._decode_length(subtype, allow_indefinite=True)
+        if length is None:
+            # Indefinite length
+            # NOTE: It may seem redundant to repeat this code to handle UTF-8
+            # strings but there is a reason to do this separately to
+            # byte-strings. Specifically, the CBOR spec states (in sec. 2.2):
+            #
+            #     Text strings with indefinite lengths act the same as byte
+            #     strings with indefinite lengths, except that all their chunks
+            #     MUST be definite-length text strings.  Note that this implies
+            #     that the bytes of a single UTF-8 character cannot be spread
+            #     between chunks: a new chunk can only be started at a
+            #     character boundary.
+            #
+            # This precludes using the indefinite bytestring decoder above as
+            # that would happily ignore UTF-8 characters split across chunks.
+            buf: list[str] = []
+            while True:
+                initial_byte = self.read(1)[0]
+                if initial_byte == 0xFF:
+                    result = "".join(buf)
+                    break
+                elif initial_byte >> 5 == 3:
+                    length = self._decode_length(initial_byte & 0x1F)
+                    if length is None or length > sys.maxsize:
+                        raise CBORDecodeValueError(
+                            "invalid length for indefinite string chunk 0x%x" % length
+                        )
+
+                    value = self.read(length).decode("utf-8", self._str_errors)
+                    buf.append(value)
+                else:
+                    raise CBORDecodeValueError(
+                        "non-string found in indefinite length string"
+                    )
+        else:
+            if length > sys.maxsize:
+                raise CBORDecodeValueError("invalid length for string 0x%x" % length)
+
+            result = self.read(length).decode("utf-8", self._str_errors)
+            self._stringref_namespace_add(result, length)
+
+        return self.set_shareable(result)
+
+    def decode_array(self, subtype: int) -> Sequence[Any]:
+        # Major tag 4
+        length = self._decode_length(subtype, allow_indefinite=True)
+        if length is None:
+            # Indefinite length
+            items: list[Any] = []
+            if not self._immutable:
+                self.set_shareable(items)
+            while True:
+                value = self._decode()
+                if value is break_marker:
+                    break
+                else:
+                    items.append(value)
+        else:
+            if length > sys.maxsize:
+                raise CBORDecodeValueError("invalid length for array 0x%x" % length)
+
+            items = []
+            if not self._immutable:
+                self.set_shareable(items)
+
+            for index in range(length):
+                items.append(self._decode())
+
+        if self._immutable:
+            items_tuple = tuple(items)
+            self.set_shareable(items_tuple)
+            return items_tuple
+
+        return items
+
+    def decode_map(self, subtype: int) -> Mapping[Any, Any]:
+        # Major tag 5
+        length = self._decode_length(subtype, allow_indefinite=True)
+        if length is None:
+            # Indefinite length
+            dictionary: dict[Any, Any] = {}
+            self.set_shareable(dictionary)
+            while True:
+                key = self._decode(immutable=True, unshared=True)
+                if key is break_marker:
+                    break
+                else:
+                    dictionary[key] = self._decode(unshared=True)
+        else:
+            dictionary = {}
+            self.set_shareable(dictionary)
+            for _ in range(length):
+                key = self._decode(immutable=True, unshared=True)
+                dictionary[key] = self._decode(unshared=True)
+
+        if self._object_hook:
+            dictionary = self._object_hook(self, dictionary)
+            self.set_shareable(dictionary)
+        elif self._immutable:
+            frozen_dict = FrozenDict(dictionary)
+            self.set_shareable(dictionary)
+            return frozen_dict
+
+        return dictionary
+
+    def decode_semantic(self, subtype: int) -> Any:
+        # Major tag 6
+        tagnum = self._decode_length(subtype)
+        if semantic_decoder := semantic_decoders.get(tagnum):
+            return semantic_decoder(self)
+
+        tag = CBORTag(tagnum, None)
+        self.set_shareable(tag)
+        tag.value = self._decode(unshared=True)
+        if self._tag_hook:
+            tag = self._tag_hook(self, tag)
+
+        return self.set_shareable(tag)
+
+    def decode_special(self, subtype: int) -> Any:
+        # Simple value
+        if subtype < 20:
+            # XXX Set shareable?
+            return CBORSimpleValue(subtype)
+
+        # Major tag 7
+        try:
+            return special_decoders[subtype](self)
+        except KeyError as e:
+            raise CBORDecodeValueError(
+                "Undefined Reserved major type 7 subtype 0x%x" % subtype
+            ) from e
+
+    #
+    # Semantic decoders (major tag 6)
+    #
+
+    def decode_datetime_string(self) -> datetime:
+        # Semantic tag 0
+        value = self._decode()
+        match = timestamp_re.match(value)
+        if match:
+            (
+                year,
+                month,
+                day,
+                hour,
+                minute,
+                second,
+                secfrac,
+                offset_sign,
+                offset_h,
+                offset_m,
+            ) = match.groups()
+            if secfrac is None:
+                microsecond = 0
+            else:
+                microsecond = int(f"{secfrac:<06}")
+
+            if offset_h:
+                if offset_sign == "-":
+                    sign = -1
+                else:
+                    sign = 1
+                hours = int(offset_h) * sign
+                minutes = int(offset_m) * sign
+                tz = timezone(timedelta(hours=hours, minutes=minutes))
+            else:
+                tz = timezone.utc
+
+            return self.set_shareable(
+                datetime(
+                    int(year),
+                    int(month),
+                    int(day),
+                    int(hour),
+                    int(minute),
+                    int(second),
+                    microsecond,
+                    tz,
+                )
+            )
+        else:
+            raise CBORDecodeValueError(f"invalid datetime string: {value!r}")
+
+    def decode_epoch_datetime(self) -> datetime:
+        # Semantic tag 1
+        value = self._decode()
+        return self.set_shareable(datetime.fromtimestamp(value, timezone.utc))
+
+    def decode_positive_bignum(self) -> int:
+        # Semantic tag 2
+        from binascii import hexlify
+
+        value = self._decode()
+        if not isinstance(value, bytes):
+            raise CBORDecodeValueError("invalid bignum value " + str(value))
+
+        return self.set_shareable(int(hexlify(value), 16))
+
+    def decode_negative_bignum(self) -> int:
+        # Semantic tag 3
+        return self.set_shareable(-self.decode_positive_bignum() - 1)
+
+    def decode_fraction(self) -> Decimal:
+        # Semantic tag 4
+        from decimal import Decimal
+
+        try:
+            exp, sig = self._decode()
+        except (TypeError, ValueError) as e:
+            raise CBORDecodeValueError("Incorrect tag 4 payload") from e
+        tmp = Decimal(sig).as_tuple()
+        return self.set_shareable(Decimal((tmp.sign, tmp.digits, exp)))
+
+    def decode_bigfloat(self) -> Decimal:
+        # Semantic tag 5
+        from decimal import Decimal
+
+        try:
+            exp, sig = self._decode()
+        except (TypeError, ValueError) as e:
+            raise CBORDecodeValueError("Incorrect tag 5 payload") from e
+
+        return self.set_shareable(Decimal(sig) * (2 ** Decimal(exp)))
+
+    def decode_stringref(self) -> str | bytes:
+        # Semantic tag 25
+        if self._stringref_namespace is None:
+            raise CBORDecodeValueError("string reference outside of namespace")
+
+        index: int = self._decode()
+        try:
+            value = self._stringref_namespace[index]
+        except IndexError:
+            raise CBORDecodeValueError("string reference %d not found" % index)
+
+        return value
+
+    def decode_shareable(self) -> object:
+        # Semantic tag 28
+        old_index = self._share_index
+        self._share_index = len(self._shareables)
+        self._shareables.append(None)
+        try:
+            return self._decode()
+        finally:
+            self._share_index = old_index
+
+    def decode_sharedref(self) -> Any:
+        # Semantic tag 29
+        value = self._decode(unshared=True)
+        try:
+            shared = self._shareables[value]
+        except IndexError:
+            raise CBORDecodeValueError("shared reference %d not found" % value)
+
+        if shared is None:
+            raise CBORDecodeValueError(
+                "shared value %d has not been initialized" % value
+            )
+        else:
+            return shared
+
+    def decode_rational(self) -> Fraction:
+        # Semantic tag 30
+        from fractions import Fraction
+
+        return self.set_shareable(Fraction(*self._decode()))
+
+    def decode_regexp(self) -> re.Pattern[str]:
+        # Semantic tag 35
+        return self.set_shareable(re.compile(self._decode()))
+
+    def decode_mime(self) -> Message:
+        # Semantic tag 36
+        from email.parser import Parser
+
+        return self.set_shareable(Parser().parsestr(self._decode()))
+
+    def decode_uuid(self) -> UUID:
+        # Semantic tag 37
+        from uuid import UUID
+
+        return self.set_shareable(UUID(bytes=self._decode()))
+
+    def decode_stringref_namespace(self) -> Any:
+        # Semantic tag 256
+        old_namespace = self._stringref_namespace
+        self._stringref_namespace = []
+        value = self._decode()
+        self._stringref_namespace = old_namespace
+        return value
+
+    def decode_set(self) -> set[Any] | frozenset[Any]:
+        # Semantic tag 258
+        if self._immutable:
+            return self.set_shareable(frozenset(self._decode(immutable=True)))
+        else:
+            return self.set_shareable(set(self._decode(immutable=True)))
+
+    def decode_ipaddress(self) -> IPv4Address | IPv6Address | CBORTag:
+        # Semantic tag 260
+        from ipaddress import ip_address
+
+        buf = self.decode()
+        if not isinstance(buf, bytes) or len(buf) not in (4, 6, 16):
+            raise CBORDecodeValueError(f"invalid ipaddress value {buf!r}")
+        elif len(buf) in (4, 16):
+            return self.set_shareable(ip_address(buf))
+        elif len(buf) == 6:
+            # MAC address
+            return self.set_shareable(CBORTag(260, buf))
+
+        raise CBORDecodeValueError(f"invalid ipaddress value {buf!r}")
+
+    def decode_ipnetwork(self) -> IPv4Network | IPv6Network:
+        # Semantic tag 261
+        from ipaddress import ip_network
+
+        net_map = self.decode()
+        if isinstance(net_map, Mapping) and len(net_map) == 1:
+            for net in net_map.items():
+                try:
+                    return self.set_shareable(ip_network(net, strict=False))
+                except (TypeError, ValueError):
+                    break
+
+        raise CBORDecodeValueError(f"invalid ipnetwork value {net_map!r}")
+
+    def decode_self_describe_cbor(self) -> Any:
+        # Semantic tag 55799
+        return self._decode()
+
+    #
+    # Special decoders (major tag 7)
+    #
+
+    def decode_simple_value(self) -> CBORSimpleValue:
+        # XXX Set shareable?
+        return CBORSimpleValue(self.read(1)[0])
+
+    def decode_float16(self) -> float:
+        return self.set_shareable(cast(float, struct.unpack(">e", self.read(2))[0]))
+
+    def decode_float32(self) -> float:
+        return self.set_shareable(cast(float, struct.unpack(">f", self.read(4))[0]))
+
+    def decode_float64(self) -> float:
+        return self.set_shareable(cast(float, struct.unpack(">d", self.read(8))[0]))
+
+
+major_decoders: dict[int, Callable[[CBORDecoder, int], Any]] = {
+    0: CBORDecoder.decode_uint,
+    1: CBORDecoder.decode_negint,
+    2: CBORDecoder.decode_bytestring,
+    3: CBORDecoder.decode_string,
+    4: CBORDecoder.decode_array,
+    5: CBORDecoder.decode_map,
+    6: CBORDecoder.decode_semantic,
+    7: CBORDecoder.decode_special,
+}
+
+special_decoders: dict[int, Callable[[CBORDecoder], Any]] = {
+    20: lambda self: False,
+    21: lambda self: True,
+    22: lambda self: None,
+    23: lambda self: undefined,
+    24: CBORDecoder.decode_simple_value,
+    25: CBORDecoder.decode_float16,
+    26: CBORDecoder.decode_float32,
+    27: CBORDecoder.decode_float64,
+    31: lambda self: break_marker,
+}
+
+semantic_decoders: dict[int, Callable[[CBORDecoder], Any]] = {
+    0: CBORDecoder.decode_datetime_string,
+    1: CBORDecoder.decode_epoch_datetime,
+    2: CBORDecoder.decode_positive_bignum,
+    3: CBORDecoder.decode_negative_bignum,
+    4: CBORDecoder.decode_fraction,
+    5: CBORDecoder.decode_bigfloat,
+    25: CBORDecoder.decode_stringref,
+    28: CBORDecoder.decode_shareable,
+    29: CBORDecoder.decode_sharedref,
+    30: CBORDecoder.decode_rational,
+    35: CBORDecoder.decode_regexp,
+    36: CBORDecoder.decode_mime,
+    37: CBORDecoder.decode_uuid,
+    256: CBORDecoder.decode_stringref_namespace,
+    258: CBORDecoder.decode_set,
+    260: CBORDecoder.decode_ipaddress,
+    261: CBORDecoder.decode_ipnetwork,
+    55799: CBORDecoder.decode_self_describe_cbor,
+}
+
+
+def loads(
+    s: bytes | bytearray | memoryview,
+    tag_hook: Callable[[CBORDecoder, CBORTag], Any] | None = None,
+    object_hook: Callable[[CBORDecoder, dict[Any, Any]], Any] | None = None,
+    str_errors: Literal["strict", "error", "replace"] = "strict",
+) -> object:
+    """
+    Deserialize an object from a bytestring.
+
+    :param bytes s:
+        the bytestring to deserialize
+    :param tag_hook:
+        callable that takes 2 arguments: the decoder instance, and the :class:`.CBORTag`
+        to be decoded. This callback is invoked for any tags for which there is no
+        built-in decoder. The return value is substituted for the :class:`.CBORTag`
+        object in the deserialized output
+    :param object_hook:
+        callable that takes 2 arguments: the decoder instance, and a dictionary. This
+        callback is invoked for each deserialized :class:`dict` object. The return value
+        is substituted for the dict in the deserialized output.
+    :param str_errors:
+        determines how to handle unicode decoding errors (see the `Error Handlers`_
+        section in the standard library documentation for details)
+    :return:
+        the deserialized object
+
+    .. _Error Handlers: https://docs.python.org/3/library/codecs.html#error-handlers
+
+    """
+    with BytesIO(s) as fp:
+        return CBORDecoder(
+            fp, tag_hook=tag_hook, object_hook=object_hook, str_errors=str_errors
+        ).decode()
+
+
+def load(
+    fp: BytesIO,
+    tag_hook: Callable[[CBORDecoder, CBORTag], Any] | None = None,
+    object_hook: Callable[[CBORDecoder, dict[Any, Any]], Any] | None = None,
+    str_errors: Literal["strict", "error", "replace"] = "strict",
+) -> object:
+    """
+    Deserialize an object from an open file.
+
+    :param fp:
+        the file to read from (any file-like object opened for reading in binary mode)
+    :param tag_hook:
+        callable that takes 2 arguments: the decoder instance, and the :class:`.CBORTag`
+        to be decoded. This callback is invoked for any tags for which there is no
+        built-in decoder. The return value is substituted for the :class:`.CBORTag`
+        object in the deserialized output
+    :param object_hook:
+        callable that takes 2 arguments: the decoder instance, and a dictionary. This
+        callback is invoked for each deserialized :class:`dict` object. The return value
+        is substituted for the dict in the deserialized output.
+    :param str_errors:
+        determines how to handle unicode decoding errors (see the `Error Handlers`_
+        section in the standard library documentation for details)
+    :return:
+        the deserialized object
+
+    .. _Error Handlers: https://docs.python.org/3/library/codecs.html#error-handlers
+
+    """
+    return CBORDecoder(
+        fp, tag_hook=tag_hook, object_hook=object_hook, str_errors=str_errors
+    ).decode()
diff --git a/cbor2/_encoder.py b/cbor2/_encoder.py
new file mode 100644
index 00000000..8f34a031
--- /dev/null
+++ b/cbor2/_encoder.py
@@ -0,0 +1,812 @@
+from __future__ import annotations
+
+import math
+import re
+import struct
+import sys
+from collections import OrderedDict, defaultdict
+from collections.abc import Callable, Generator, Mapping, Sequence, Set
+from contextlib import contextmanager
+from datetime import date, datetime, time, tzinfo
+from functools import wraps
+from io import BytesIO
+from sys import modules
+from typing import TYPE_CHECKING, Any, cast
+
+from ._types import (
+    CBOREncodeTypeError,
+    CBOREncodeValueError,
+    CBORSimpleValue,
+    CBORTag,
+    FrozenDict,
+    UndefinedType,
+    undefined,
+)
+
+if TYPE_CHECKING:
+    from decimal import Decimal
+    from email.message import Message
+    from fractions import Fraction
+    from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
+    from uuid import UUID
+
+    if sys.version_info >= (3, 12):
+        from typing import Buffer
+    else:
+        from typing_extensions import Buffer
+
+
+def shareable_encoder(
+    func: Callable[[CBOREncoder, Any], None]
+) -> Callable[[CBOREncoder, Any], None]:
+    """
+    Wrap the given encoder function to gracefully handle cyclic data
+    structures.
+
+    If value sharing is enabled, this marks the given value shared in the
+    datastream on the first call. If the value has already been passed to this
+    method, a reference marker is instead written to the data stream and the
+    wrapped function is not called.
+
+    If value sharing is disabled, only infinite recursion protection is done.
+    :rtype: Callable[[cbor2.CBOREncoder, Any], None]
+    """
+
+    @wraps(func)
+    def wrapper(encoder: CBOREncoder, value: Any) -> None:
+        encoder.encode_shared(func, value)
+
+    return wrapper
+
+
+def container_encoder(
+    func: Callable[[CBOREncoder, Any], Any]
+) -> Callable[[CBOREncoder, Any], Any]:
+    """
+    The given encoder is a container with child values. Handle cyclic or
+    duplicate references to the value and strings within the value
+    efficiently.
+
+    Containers may contain cyclic data structures or may contain values
+    or themselves by referenced multiple times throughout the greater
+    encoded value and could thus be more efficiently encoded with shared
+    value references and string references where duplication occurs.
+
+    If value sharing is enabled, this marks the given value shared in the
+    datastream on the first call. If the value has already been passed to this
+    method, a reference marker is instead written to the data stream and the
+    wrapped function is not called.
+
+    If value sharing is disabled, only infinite recursion protection is done.
+
+    If string referencing is enabled and this is the first use of this
+    method in encoding a value, all repeated references to long strings
+    and bytearrays will be replaced with references to the first
+    occurrence of those arrays.
+
+    If string referencing is disabled, all strings and bytearrays will
+    be encoded directly.
+    """
+
+    @wraps(func)
+    def wrapper(encoder: CBOREncoder, value: Any) -> None:
+        encoder.encode_container(func, value)
+
+    return wrapper
+
+
+class CBOREncoder:
+    """
+    The CBOREncoder class implements a fully featured `CBOR`_ encoder with
+    several extensions for handling shared references, big integers, rational
+    numbers and so on. Typically the class is not used directly, but the
+    :func:`dump` and :func:`dumps` functions are called to indirectly construct
+    and use the class.
+
+    When the class is constructed manually, the main entry points are
+    :meth:`encode` and :meth:`encode_to_bytes`.
+
+    .. _CBOR: https://cbor.io/
+    """
+
+    __slots__ = (
+        "datetime_as_timestamp",
+        "_timezone",
+        "_default",
+        "value_sharing",
+        "_fp",
+        "_fp_write",
+        "_shared_containers",
+        "_encoders",
+        "_canonical",
+        "string_referencing",
+        "string_namespacing",
+        "_string_references",
+    )
+
+    _fp: BytesIO
+    _fp_write: Callable[[Buffer], int]
+
+    def __init__(
+        self,
+        fp: BytesIO,
+        datetime_as_timestamp: bool = False,
+        timezone: tzinfo | None = None,
+        value_sharing: bool = False,
+        default: Callable[[CBOREncoder, Any], Any] | None = None,
+        canonical: bool = False,
+        date_as_datetime: bool = False,
+        string_referencing: bool = False,
+    ):
+        """
+        :param fp:
+            the file to write to (any file-like object opened for writing in binary
+            mode)
+        :param datetime_as_timestamp:
+            set to ``True`` to serialize datetimes as UNIX timestamps (this makes
+            datetimes more concise on the wire, but loses the timezone information)
+        :param timezone:
+            the default timezone to use for serializing naive datetimes; if this is not
+            specified naive datetimes will throw a :exc:`ValueError` when encoding is
+            attempted
+        :param value_sharing:
+            set to ``True`` to allow more efficient serializing of repeated values and,
+            more importantly, cyclic data structures, at the cost of extra line overhead
+        :param default:
+            a callable that is called by the encoder with two arguments (the encoder
+            instance and the value being encoded) when no suitable encoder has been
+            found, and should use the methods on the encoder to encode any objects it
+            wants to add to the data stream
+        :param canonical:
+            when ``True``, use "canonical" CBOR representation; this typically involves
+            sorting maps, sets, etc. into a pre-determined order ensuring that
+            serializations are comparable without decoding
+        :param date_as_datetime:
+            set to ``True`` to serialize date objects as datetimes (CBOR tag 0), which
+            was the default behavior in previous releases (cbor2 <= 4.1.2).
+        :param string_referencing:
+            set to ``True`` to allow more efficient serializing of repeated string
+            values
+
+        """
+        self.fp = fp
+        self.datetime_as_timestamp = datetime_as_timestamp
+        self.timezone = timezone
+        self.value_sharing = value_sharing
+        self.string_referencing = string_referencing
+        self.string_namespacing = string_referencing
+        self.default = default
+        self._canonical = canonical
+        self._shared_containers: dict[
+            int, tuple[object, int | None]
+        ] = {}  # indexes used for value sharing
+        self._string_references: dict[
+            str | bytes, int
+        ] = {}  # indexes used for string references
+        self._encoders = default_encoders.copy()
+        if canonical:
+            self._encoders.update(canonical_encoders)
+        if date_as_datetime:
+            self._encoders[date] = CBOREncoder.encode_date
+
+    def _find_encoder(
+        self, obj_type: type
+    ) -> Callable[[CBOREncoder, Any], None] | None:
+        for type_or_tuple, enc in list(self._encoders.items()):
+            if type(type_or_tuple) is tuple:
+                try:
+                    modname, typename = type_or_tuple
+                except (TypeError, ValueError):
+                    raise CBOREncodeValueError(
+                        f"invalid deferred encoder type {type_or_tuple!r} (must be a "
+                        "2-tuple of module name and type name, e.g. "
+                        "('collections', 'defaultdict'))"
+                    )
+
+                imported_type = getattr(modules.get(modname), typename, None)
+                if imported_type is not None:
+                    del self._encoders[type_or_tuple]
+                    self._encoders[imported_type] = enc
+                    type_ = imported_type
+                else:  # pragma: nocover
+                    continue
+            else:
+                type_ = type_or_tuple
+
+            if issubclass(obj_type, type_):
+                self._encoders[obj_type] = enc
+                return enc
+
+        return None
+
+    @property
+    def fp(self) -> BytesIO:
+        return self._fp
+
+    @fp.setter
+    def fp(self, value: BytesIO) -> None:
+        try:
+            if not callable(value.write):
+                raise ValueError("fp.write is not callable")
+        except AttributeError:
+            raise ValueError("fp object has no write method")
+        else:
+            self._fp = value
+            self._fp_write = value.write
+
+    @property
+    def timezone(self) -> tzinfo | None:
+        return self._timezone
+
+    @timezone.setter
+    def timezone(self, value: tzinfo | None) -> None:
+        if value is None or isinstance(value, tzinfo):
+            self._timezone = value
+        else:
+            raise ValueError("timezone must be None or a tzinfo instance")
+
+    @property
+    def default(self) -> Callable[[CBOREncoder, Any], Any] | None:
+        return self._default
+
+    @default.setter
+    def default(self, value: Callable[[CBOREncoder, Any], Any] | None) -> None:
+        if value is None or callable(value):
+            self._default = value
+        else:
+            raise ValueError("default must be None or a callable")
+
+    @property
+    def canonical(self) -> bool:
+        return self._canonical
+
+    @contextmanager
+    def disable_value_sharing(self) -> Generator[None, None, None]:
+        """
+        Disable value sharing in the encoder for the duration of the context
+        block.
+        """
+        old_value_sharing = self.value_sharing
+        self.value_sharing = False
+        yield
+        self.value_sharing = old_value_sharing
+
+    @contextmanager
+    def disable_string_referencing(self) -> Generator[None, None, None]:
+        """
+        Disable tracking of string references for the duration of the
+        context block.
+        """
+        old_string_referencing = self.string_referencing
+        self.string_referencing = False
+        yield
+        self.string_referencing = old_string_referencing
+
+    @contextmanager
+    def disable_string_namespacing(self) -> Generator[None, None, None]:
+        """
+        Disable generation of new string namespaces for the duration of the
+        context block.
+        """
+        old_string_namespacing = self.string_namespacing
+        self.string_namespacing = False
+        yield
+        self.string_namespacing = old_string_namespacing
+
+    def write(self, data: bytes) -> None:
+        """
+        Write bytes to the data stream.
+
+        :param bytes data:
+            the bytes to write
+        """
+        self._fp_write(data)
+
+    def encode(self, obj: Any) -> None:
+        """
+        Encode the given object using CBOR.
+
+        :param obj:
+            the object to encode
+        """
+        obj_type = obj.__class__
+        encoder = (
+            self._encoders.get(obj_type)
+            or self._find_encoder(obj_type)
+            or self._default
+        )
+        if not encoder:
+            raise CBOREncodeTypeError("cannot serialize type %s" % obj_type.__name__)
+
+        encoder(self, obj)
+
+    def encode_to_bytes(self, obj: Any) -> bytes:
+        """
+        Encode the given object to a byte buffer and return its value as bytes.
+
+        This method was intended to be used from the ``default`` hook when an
+        object needs to be encoded separately from the rest but while still
+        taking advantage of the shared value registry.
+        """
+        with BytesIO() as fp:
+            old_fp = self.fp
+            self.fp = fp
+            self.encode(obj)
+            self.fp = old_fp
+            return fp.getvalue()
+
+    def encode_container(
+        self, encoder: Callable[[CBOREncoder, Any], Any], value: Any
+    ) -> None:
+        if self.string_namespacing:
+            # Create a new string reference domain
+            self.encode_length(6, 256)
+
+        with self.disable_string_namespacing():
+            self.encode_shared(encoder, value)
+
+    def encode_shared(
+        self, encoder: Callable[[CBOREncoder, Any], Any], value: Any
+    ) -> None:
+        value_id = id(value)
+        try:
+            index = self._shared_containers[id(value)][1]
+        except KeyError:
+            if self.value_sharing:
+                # Mark the container as shareable
+                self._shared_containers[value_id] = (
+                    value,
+                    len(self._shared_containers),
+                )
+                self.encode_length(6, 0x1C)
+                encoder(self, value)
+            else:
+                self._shared_containers[value_id] = (value, None)
+                try:
+                    encoder(self, value)
+                finally:
+                    del self._shared_containers[value_id]
+        else:
+            if self.value_sharing:
+                # Generate a reference to the previous index instead of
+                # encoding this again
+                self.encode_length(6, 0x1D)
+                self.encode_int(cast(int, index))
+            else:
+                raise CBOREncodeValueError(
+                    "cyclic data structure detected but value sharing is disabled"
+                )
+
+    def _stringref(self, value: str | bytes) -> bool:
+        """
+        Try to encode the string or bytestring as a reference.
+
+        Returns True if a reference was generated, False if the string
+        must still be emitted.
+        """
+        try:
+            index = self._string_references[value]
+            self.encode_semantic(CBORTag(25, index))
+            return True
+        except KeyError:
+            length = len(value)
+            next_index = len(self._string_references)
+            if next_index < 24:
+                is_referenced = length >= 3
+            elif next_index < 256:
+                is_referenced = length >= 4
+            elif next_index < 65536:
+                is_referenced = length >= 5
+            elif next_index < 4294967296:
+                is_referenced = length >= 7
+            else:
+                is_referenced = length >= 11
+
+            if is_referenced:
+                self._string_references[value] = next_index
+
+            return False
+
+    def encode_length(self, major_tag: int, length: int) -> None:
+        major_tag <<= 5
+        if length < 24:
+            self._fp_write(struct.pack(">B", major_tag | length))
+        elif length < 256:
+            self._fp_write(struct.pack(">BB", major_tag | 24, length))
+        elif length < 65536:
+            self._fp_write(struct.pack(">BH", major_tag | 25, length))
+        elif length < 4294967296:
+            self._fp_write(struct.pack(">BL", major_tag | 26, length))
+        else:
+            self._fp_write(struct.pack(">BQ", major_tag | 27, length))
+
+    def encode_int(self, value: int) -> None:
+        # Big integers (2 ** 64 and over)
+        if value >= 18446744073709551616 or value < -18446744073709551616:
+            if value >= 0:
+                major_type = 0x02
+            else:
+                major_type = 0x03
+                value = -value - 1
+
+            payload = value.to_bytes((value.bit_length() + 7) // 8, "big")
+            self.encode_semantic(CBORTag(major_type, payload))
+        elif value >= 0:
+            self.encode_length(0, value)
+        else:
+            self.encode_length(1, -(value + 1))
+
+    def encode_bytestring(self, value: bytes) -> None:
+        if self.string_referencing:
+            if self._stringref(value):
+                return
+
+        self.encode_length(2, len(value))
+        self._fp_write(value)
+
+    def encode_bytearray(self, value: bytearray) -> None:
+        self.encode_bytestring(bytes(value))
+
+    def encode_string(self, value: str) -> None:
+        if self.string_referencing:
+            if self._stringref(value):
+                return
+
+        encoded = value.encode("utf-8")
+        self.encode_length(3, len(encoded))
+        self._fp_write(encoded)
+
+    @container_encoder
+    def encode_array(self, value: Sequence[Any]) -> None:
+        self.encode_length(4, len(value))
+        for item in value:
+            self.encode(item)
+
+    @container_encoder
+    def encode_map(self, value: Mapping[Any, Any]) -> None:
+        self.encode_length(5, len(value))
+        for key, val in value.items():
+            self.encode(key)
+            self.encode(val)
+
+    def encode_sortable_key(self, value: Any) -> tuple[int, bytes]:
+        """
+        Takes a key and calculates the length of its optimal byte
+        representation, along with the representation itself. This is used as
+        the sorting key in CBOR's canonical representations.
+        """
+        with self.disable_string_referencing():
+            encoded = self.encode_to_bytes(value)
+            return len(encoded), encoded
+
+    @container_encoder
+    def encode_canonical_map(self, value: Mapping[Any, Any]) -> None:
+        """Reorder keys according to Canonical CBOR specification"""
+        keyed_keys = (
+            (self.encode_sortable_key(key), key, value) for key, value in value.items()
+        )
+        self.encode_length(5, len(value))
+        for sortkey, realkey, value in sorted(keyed_keys):
+            if self.string_referencing:
+                # String referencing requires that the order encoded is
+                # the same as the order emitted so string references are
+                # generated after an order is determined
+                self.encode(realkey)
+            else:
+                self._fp_write(sortkey[1])
+            self.encode(value)
+
+    def encode_semantic(self, value: CBORTag) -> None:
+        # Nested string reference domains are distinct
+        old_string_referencing = self.string_referencing
+        old_string_references = self._string_references
+        if value.tag == 256:
+            self.string_referencing = True
+            self._string_references = {}
+
+        self.encode_length(6, value.tag)
+        self.encode(value.value)
+
+        self.string_referencing = old_string_referencing
+        self._string_references = old_string_references
+
+    #
+    # Semantic decoders (major tag 6)
+    #
+
+    def encode_datetime(self, value: datetime) -> None:
+        # Semantic tag 0
+        if not value.tzinfo:
+            if self._timezone:
+                value = value.replace(tzinfo=self._timezone)
+            else:
+                raise CBOREncodeValueError(
+                    f"naive datetime {value!r} encountered and no default timezone "
+                    "has been set"
+                )
+
+        if self.datetime_as_timestamp:
+            from calendar import timegm
+
+            if not value.microsecond:
+                timestamp: float = timegm(value.utctimetuple())
+            else:
+                timestamp = timegm(value.utctimetuple()) + value.microsecond / 1000000
+
+            self.encode_semantic(CBORTag(1, timestamp))
+        else:
+            datestring = value.isoformat().replace("+00:00", "Z")
+            self.encode_semantic(CBORTag(0, datestring))
+
+    def encode_date(self, value: date) -> None:
+        value = datetime.combine(value, time()).replace(tzinfo=self._timezone)
+        self.encode_datetime(value)
+
+    def encode_decimal(self, value: Decimal) -> None:
+        # Semantic tag 4
+        if value.is_nan():
+            self._fp_write(b"\xf9\x7e\x00")
+        elif value.is_infinite():
+            self._fp_write(b"\xf9\x7c\x00" if value > 0 else b"\xf9\xfc\x00")
+        else:
+            dt = value.as_tuple()
+            sig = 0
+            for digit in dt.digits:
+                sig = (sig * 10) + digit
+            if dt.sign:
+                sig = -sig
+            with self.disable_value_sharing():
+                self.encode_semantic(CBORTag(4, [dt.exponent, sig]))
+
+    def encode_stringref(self, value: str | bytes) -> None:
+        # Semantic tag 25
+        if not self._stringref(value):
+            self.encode(value)
+
+    def encode_rational(self, value: Fraction) -> None:
+        # Semantic tag 30
+        with self.disable_value_sharing():
+            self.encode_semantic(CBORTag(30, [value.numerator, value.denominator]))
+
+    def encode_regexp(self, value: re.Pattern[str]) -> None:
+        # Semantic tag 35
+        self.encode_semantic(CBORTag(35, str(value.pattern)))
+
+    def encode_mime(self, value: Message) -> None:
+        # Semantic tag 36
+        self.encode_semantic(CBORTag(36, value.as_string()))
+
+    def encode_uuid(self, value: UUID) -> None:
+        # Semantic tag 37
+        self.encode_semantic(CBORTag(37, value.bytes))
+
+    def encode_stringref_namespace(self, value: Any) -> None:
+        # Semantic tag 256
+        with self.disable_string_namespacing():
+            self.encode_semantic(CBORTag(256, value))
+
+    def encode_set(self, value: Set[Any]) -> None:
+        # Semantic tag 258
+        self.encode_semantic(CBORTag(258, tuple(value)))
+
+    def encode_canonical_set(self, value: Set[Any]) -> None:
+        # Semantic tag 258
+        values = sorted((self.encode_sortable_key(key), key) for key in value)
+        self.encode_semantic(CBORTag(258, [key[1] for key in values]))
+
+    def encode_ipaddress(self, value: IPv4Address | IPv6Address) -> None:
+        # Semantic tag 260
+        self.encode_semantic(CBORTag(260, value.packed))
+
+    def encode_ipnetwork(self, value: IPv4Network | IPv6Network) -> None:
+        # Semantic tag 261
+        self.encode_semantic(
+            CBORTag(261, {value.network_address.packed: value.prefixlen})
+        )
+
+    #
+    # Special encoders (major tag 7)
+    #
+
+    def encode_simple_value(self, value: CBORSimpleValue) -> None:
+        if value.value < 20:
+            self._fp_write(struct.pack(">B", 0xE0 | value.value))
+        else:
+            self._fp_write(struct.pack(">BB", 0xF8, value.value))
+
+    def encode_float(self, value: float) -> None:
+        # Handle special values efficiently
+        if math.isnan(value):
+            self._fp_write(b"\xf9\x7e\x00")
+        elif math.isinf(value):
+            self._fp_write(b"\xf9\x7c\x00" if value > 0 else b"\xf9\xfc\x00")
+        else:
+            self._fp_write(struct.pack(">Bd", 0xFB, value))
+
+    def encode_minimal_float(self, value: float) -> None:
+        # Handle special values efficiently
+        if math.isnan(value):
+            self._fp_write(b"\xf9\x7e\x00")
+        elif math.isinf(value):
+            self._fp_write(b"\xf9\x7c\x00" if value > 0 else b"\xf9\xfc\x00")
+        else:
+            # Try each encoding in turn from longest to shortest
+            encoded = struct.pack(">Bd", 0xFB, value)
+            for format, tag in [(">Bf", 0xFA), (">Be", 0xF9)]:
+                try:
+                    new_encoded = struct.pack(format, tag, value)
+                    # Check if encoding as low-byte float loses precision
+                    if struct.unpack(format, new_encoded)[1] == value:
+                        encoded = new_encoded
+                    else:
+                        break
+                except OverflowError:
+                    break
+
+            self._fp_write(encoded)
+
+    def encode_boolean(self, value: bool) -> None:
+        self._fp_write(b"\xf5" if value else b"\xf4")
+
+    def encode_none(self, value: None) -> None:
+        self._fp_write(b"\xf6")
+
+    def encode_undefined(self, value: UndefinedType) -> None:
+        self._fp_write(b"\xf7")
+
+
+default_encoders: dict[type | tuple[str, str], Callable[[CBOREncoder, Any], None]] = {
+    bytes: CBOREncoder.encode_bytestring,
+    bytearray: CBOREncoder.encode_bytearray,
+    str: CBOREncoder.encode_string,
+    int: CBOREncoder.encode_int,
+    float: CBOREncoder.encode_float,
+    ("decimal", "Decimal"): CBOREncoder.encode_decimal,
+    bool: CBOREncoder.encode_boolean,
+    type(None): CBOREncoder.encode_none,
+    tuple: CBOREncoder.encode_array,
+    list: CBOREncoder.encode_array,
+    dict: CBOREncoder.encode_map,
+    defaultdict: CBOREncoder.encode_map,
+    OrderedDict: CBOREncoder.encode_map,
+    FrozenDict: CBOREncoder.encode_map,
+    type(undefined): CBOREncoder.encode_undefined,
+    datetime: CBOREncoder.encode_datetime,
+    re.Pattern: CBOREncoder.encode_regexp,
+    ("fractions", "Fraction"): CBOREncoder.encode_rational,
+    ("email.message", "Message"): CBOREncoder.encode_mime,
+    ("uuid", "UUID"): CBOREncoder.encode_uuid,
+    ("ipaddress", "IPv4Address"): CBOREncoder.encode_ipaddress,
+    ("ipaddress", "IPv6Address"): CBOREncoder.encode_ipaddress,
+    ("ipaddress", "IPv4Network"): CBOREncoder.encode_ipnetwork,
+    ("ipaddress", "IPv6Network"): CBOREncoder.encode_ipnetwork,
+    CBORSimpleValue: CBOREncoder.encode_simple_value,
+    CBORTag: CBOREncoder.encode_semantic,
+    set: CBOREncoder.encode_set,
+    frozenset: CBOREncoder.encode_set,
+}
+
+
+canonical_encoders: dict[type | tuple[str, str], Callable[[CBOREncoder, Any], None]] = {
+    float: CBOREncoder.encode_minimal_float,
+    dict: CBOREncoder.encode_canonical_map,
+    defaultdict: CBOREncoder.encode_canonical_map,
+    OrderedDict: CBOREncoder.encode_canonical_map,
+    FrozenDict: CBOREncoder.encode_canonical_map,
+    set: CBOREncoder.encode_canonical_set,
+    frozenset: CBOREncoder.encode_canonical_set,
+}
+
+
+def dumps(
+    obj: object,
+    datetime_as_timestamp: bool = False,
+    timezone: tzinfo | None = None,
+    value_sharing: bool = False,
+    default: Callable[[CBOREncoder, Any], None] | None = None,
+    canonical: bool = False,
+    date_as_datetime: bool = False,
+    string_referencing: bool = False,
+) -> bytes:
+    """
+    Serialize an object to a bytestring.
+
+    :param obj:
+        the object to serialize
+    :param datetime_as_timestamp:
+        set to ``True`` to serialize datetimes as UNIX timestamps (this makes datetimes
+        more concise on the wire, but loses the timezone information)
+    :param timezone:
+        the default timezone to use for serializing naive datetimes; if this is not
+        specified naive datetimes will throw a :exc:`ValueError` when encoding is
+        attempted
+    :param value_sharing:
+        set to ``True`` to allow more efficient serializing of repeated values
+        and, more importantly, cyclic data structures, at the cost of extra
+        line overhead
+    :param default:
+        a callable that is called by the encoder with two arguments (the encoder
+        instance and the value being encoded) when no suitable encoder has been found,
+        and should use the methods on the encoder to encode any objects it wants to add
+        to the data stream
+    :param canonical:
+        when ``True``, use "canonical" CBOR representation; this typically involves
+        sorting maps, sets, etc. into a pre-determined order ensuring that
+        serializations are comparable without decoding
+    :param date_as_datetime:
+        set to ``True`` to serialize date objects as datetimes (CBOR tag 0), which was
+        the default behavior in previous releases (cbor2 <= 4.1.2).
+    :param string_referencing:
+        set to ``True`` to allow more efficient serializing of repeated string values
+    :return: the serialized output
+
+    """
+    with BytesIO() as fp:
+        CBOREncoder(
+            fp,
+            datetime_as_timestamp=datetime_as_timestamp,
+            timezone=timezone,
+            value_sharing=value_sharing,
+            default=default,
+            canonical=canonical,
+            date_as_datetime=date_as_datetime,
+            string_referencing=string_referencing,
+        ).encode(obj)
+        return fp.getvalue()
+
+
+def dump(
+    obj: object,
+    fp: BytesIO,
+    datetime_as_timestamp: bool = False,
+    timezone: tzinfo | None = None,
+    value_sharing: bool = False,
+    default: Callable[[CBOREncoder, Any], None] | None = None,
+    canonical: bool = False,
+    date_as_datetime: bool = False,
+    string_referencing: bool = False,
+) -> None:
+    """
+    Serialize an object to a file.
+
+    :param obj:
+        the object to serialize
+    :param fp:
+        the file to write to (any file-like object opened for writing in binary mode)
+    :param datetime_as_timestamp:
+        set to ``True`` to serialize datetimes as UNIX timestamps (this makes datetimes
+        more concise on the wire, but loses the timezone information)
+    :param timezone:
+        the default timezone to use for serializing naive datetimes; if this is not
+        specified naive datetimes will throw a :exc:`ValueError` when encoding is
+        attempted
+    :param value_sharing:
+        set to ``True`` to allow more efficient serializing of repeated values
+        and, more importantly, cyclic data structures, at the cost of extra
+        line overhead
+    :param default:
+        a callable that is called by the encoder with two arguments (the encoder
+        instance and the value being encoded) when no suitable encoder has been found,
+        and should use the methods on the encoder to encode any objects it wants to add
+        to the data stream
+    :param canonical:
+        when ``True``, use "canonical" CBOR representation; this typically involves
+        sorting maps, sets, etc. into a pre-determined order ensuring that
+        serializations are comparable without decoding
+    :param date_as_datetime:
+        set to ``True`` to serialize date objects as datetimes (CBOR tag 0), which was
+        the default behavior in previous releases (cbor2 <= 4.1.2).
+    :param string_referencing:
+        set to ``True`` to allow more efficient serializing of repeated string values
+
+    """
+    CBOREncoder(
+        fp,
+        datetime_as_timestamp=datetime_as_timestamp,
+        timezone=timezone,
+        value_sharing=value_sharing,
+        default=default,
+        canonical=canonical,
+        date_as_datetime=date_as_datetime,
+        string_referencing=string_referencing,
+    ).encode(obj)
diff --git a/cbor2/_types.py b/cbor2/_types.py
new file mode 100644
index 00000000..a39a7834
--- /dev/null
+++ b/cbor2/_types.py
@@ -0,0 +1,211 @@
+from __future__ import annotations
+
+from collections import namedtuple
+from collections.abc import Iterable, Iterator
+from functools import total_ordering
+from reprlib import recursive_repr
+from typing import Any, Mapping, TypeVar
+
+KT = TypeVar("KT")
+VT_co = TypeVar("VT_co", covariant=True)
+
+
+class CBORError(Exception):
+    """Base class for errors that occur during CBOR encoding or decoding."""
+
+
+class CBOREncodeError(CBORError):
+    """Raised for exceptions occurring during CBOR encoding."""
+
+
+class CBOREncodeTypeError(CBOREncodeError, TypeError):
+    """Raised when attempting to encode a type that cannot be serialized."""
+
+
+class CBOREncodeValueError(CBOREncodeError, ValueError):
+    """Raised when the CBOR encoder encounters an invalid value."""
+
+
+class CBORDecodeError(CBORError):
+    """Raised for exceptions occurring during CBOR decoding."""
+
+
+class CBORDecodeValueError(CBORDecodeError, ValueError):
+    """Raised when the CBOR stream being decoded contains an invalid value."""
+
+
+class CBORDecodeEOF(CBORDecodeError, EOFError):
+    """Raised when decoding unexpectedly reaches EOF."""
+
+
+@total_ordering
+class CBORTag:
+    """
+    Represents a CBOR semantic tag.
+
+    :param int tag: tag number
+    :param value: encapsulated value (any object)
+    """
+
+    __slots__ = "tag", "value"
+
+    def __init__(self, tag: str | int, value: Any) -> None:
+        if not isinstance(tag, int) or tag not in range(2**64):
+            raise TypeError("CBORTag tags must be positive integers less than 2**64")
+        self.tag = tag
+        self.value = value
+
+    def __eq__(self, other: object) -> bool:
+        if isinstance(other, CBORTag):
+            return (self.tag, self.value) == (other.tag, other.value)
+
+        return NotImplemented
+
+    def __le__(self, other: object) -> bool:
+        if isinstance(other, CBORTag):
+            return (self.tag, self.value) <= (other.tag, other.value)
+
+        return NotImplemented
+
+    @recursive_repr()
+    def __repr__(self) -> str:
+        return f"CBORTag({self.tag}, {self.value!r})"
+
+    def __hash__(self) -> int:
+        return hash((self.tag, self.value))
+
+
+class CBORSimpleValue(namedtuple("CBORSimpleValue", ["value"])):
+    """
+    Represents a CBOR "simple value".
+
+    :param int value: the value (0-255)
+    """
+
+    __slots__ = ()
+
+    value: int
+
+    def __hash__(self) -> int:
+        return hash(self.value)
+
+    def __new__(cls, value: int) -> CBORSimpleValue:
+        if value < 0 or value > 255:
+            raise TypeError("simple value out of range (0..255)")
+
+        return super().__new__(cls, value)
+
+    def __eq__(self, other: object) -> bool:
+        if isinstance(other, int):
+            return self.value == other
+        elif isinstance(other, CBORSimpleValue):
+            return self.value == other.value
+
+        return NotImplemented
+
+    def __ne__(self, other: object) -> bool:
+        if isinstance(other, int):
+            return self.value != other
+        elif isinstance(other, CBORSimpleValue):
+            return self.value != other.value
+
+        return NotImplemented
+
+    def __lt__(self, other: object) -> bool:
+        if isinstance(other, int):
+            return self.value < other
+        elif isinstance(other, CBORSimpleValue):
+            return self.value < other.value
+
+        return NotImplemented
+
+    def __le__(self, other: object) -> bool:
+        if isinstance(other, int):
+            return self.value <= other
+        elif isinstance(other, CBORSimpleValue):
+            return self.value <= other.value
+
+        return NotImplemented
+
+    def __ge__(self, other: object) -> bool:
+        if isinstance(other, int):
+            return self.value >= other
+        elif isinstance(other, CBORSimpleValue):
+            return self.value >= other.value
+
+        return NotImplemented
+
+    def __gt__(self, other: object) -> bool:
+        if isinstance(other, int):
+            return self.value > other
+        elif isinstance(other, CBORSimpleValue):
+            return self.value > other.value
+
+        return NotImplemented
+
+
+class FrozenDict(Mapping[KT, VT_co]):
+    """
+    A hashable, immutable mapping type.
+
+    The arguments to ``FrozenDict`` are processed just like those to ``dict``.
+    """
+
+    def __init__(self, *args: Mapping[KT, VT_co] | Iterable[tuple[KT, VT_co]]) -> None:
+        self._d: dict[KT, VT_co] = dict(*args)
+        self._hash: int | None = None
+
+    def __iter__(self) -> Iterator[KT]:
+        return iter(self._d)
+
+    def __len__(self) -> int:
+        return len(self._d)
+
+    def __getitem__(self, key: KT) -> VT_co:
+        return self._d[key]
+
+    def __repr__(self) -> str:
+        return f"{self.__class__.__name__}({self._d})"
+
+    def __hash__(self) -> int:
+        if self._hash is None:
+            self._hash = hash((frozenset(self), frozenset(self.values())))
+
+        return self._hash
+
+
+class UndefinedType:
+    __slots__ = ()
+
+    def __new__(cls: type[UndefinedType]) -> UndefinedType:
+        try:
+            return undefined
+        except NameError:
+            return super().__new__(cls)
+
+    def __repr__(self) -> str:
+        return "undefined"
+
+    def __bool__(self) -> bool:
+        return False
+
+
+class BreakMarkerType:
+    __slots__ = ()
+
+    def __new__(cls: type[BreakMarkerType]) -> BreakMarkerType:
+        try:
+            return break_marker
+        except NameError:
+            return super().__new__(cls)
+
+    def __repr__(self) -> str:
+        return "break_marker"
+
+    def __bool__(self) -> bool:
+        return True
+
+
+#: Represents the "undefined" value.
+undefined = UndefinedType()
+break_marker = BreakMarkerType()
diff --git a/cbor2/decoder.py b/cbor2/decoder.py
index 15a5d34e..486c75f7 100644
--- a/cbor2/decoder.py
+++ b/cbor2/decoder.py
@@ -1,792 +1,9 @@
-from __future__ import annotations
+from warnings import warn
 
-import re
-import struct
-import sys
-from collections.abc import Callable, Mapping, Sequence
-from datetime import datetime, timedelta, timezone
-from io import BytesIO
-from typing import TYPE_CHECKING, Any, TypeVar, cast, overload
+from ._decoder import CBORDecoder as CBORDecoder
+from ._decoder import load as load
+from ._decoder import loads as loads
 
-from .types import (
-    CBORDecodeEOF,
-    CBORDecodeValueError,
-    CBORSimpleValue,
-    CBORTag,
-    FrozenDict,
-    break_marker,
-    undefined,
+warn(
+    "The cbor.decoder module has been deprecated. Instead import everything directly from cbor2."
 )
-
-if TYPE_CHECKING:
-    from decimal import Decimal
-    from email.message import Message
-    from fractions import Fraction
-    from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
-    from typing import Literal
-    from uuid import UUID
-
-T = TypeVar("T")
-
-timestamp_re = re.compile(
-    r"^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)"
-    r"(?:\.(\d{1,6})\d*)?(?:Z|([+-])(\d\d):(\d\d))$"
-)
-
-
-class CBORDecoder:
-    """
-    The CBORDecoder class implements a fully featured `CBOR`_ decoder with
-    several extensions for handling shared references, big integers, rational
-    numbers and so on. Typically the class is not used directly, but the
-    :func:`load` and :func:`loads` functions are called to indirectly construct
-    and use the class.
-
-    When the class is constructed manually, the main entry points are
-    :meth:`decode` and :meth:`decode_from_bytes`.
-
-    .. _CBOR: https://cbor.io/
-    """
-
-    __slots__ = (
-        "_tag_hook",
-        "_object_hook",
-        "_share_index",
-        "_shareables",
-        "_fp",
-        "_fp_read",
-        "_immutable",
-        "_str_errors",
-        "_stringref_namespace",
-    )
-
-    _fp: BytesIO
-    _fp_read: Callable[[int], bytes]
-
-    def __init__(
-        self,
-        fp: BytesIO,
-        tag_hook: Callable[[CBORDecoder, CBORTag], Any] | None = None,
-        object_hook: Callable[[CBORDecoder, dict[Any, Any]], Any] | None = None,
-        str_errors: Literal["strict", "error", "replace"] = "strict",
-    ):
-        """
-        :param fp:
-            the file to read from (any file-like object opened for reading in binary
-            mode)
-        :param tag_hook:
-            callable that takes 2 arguments: the decoder instance, and the
-            :class:`.CBORTag` to be decoded. This callback is invoked for any tags
-            for which there is no built-in decoder. The return value is substituted
-            for the :class:`.CBORTag` object in the deserialized output
-        :param object_hook:
-            callable that takes 2 arguments: the decoder instance, and a
-            dictionary. This callback is invoked for each deserialized
-            :class:`dict` object. The return value is substituted for the dict in
-            the deserialized output.
-        :param str_errors:
-            determines how to handle unicode decoding errors (see the `Error Handlers`_
-            section in the standard library documentation for details)
-
-        .. _Error Handlers: https://docs.python.org/3/library/codecs.html#error-handlers
-
-        """
-        self.fp = fp
-        self.tag_hook = tag_hook
-        self.object_hook = object_hook
-        self.str_errors = str_errors
-        self._share_index: int | None = None
-        self._shareables: list[object] = []
-        self._stringref_namespace: list[str | bytes] | None = None
-        self._immutable = False
-
-    @property
-    def immutable(self) -> bool:
-        """
-        Used by decoders to check if the calling context requires an immutable
-        type.  Object_hook or tag_hook should raise an exception if this flag
-        is set unless the result can be safely used as a dict key.
-        """
-        return self._immutable
-
-    @property
-    def fp(self) -> BytesIO:
-        return self._fp
-
-    @fp.setter
-    def fp(self, value: BytesIO) -> None:
-        try:
-            if not callable(value.read):
-                raise ValueError("fp.read is not callable")
-        except AttributeError:
-            raise ValueError("fp object has no read method")
-        else:
-            self._fp = value
-            self._fp_read = value.read
-
-    @property
-    def tag_hook(self) -> Callable[[CBORDecoder, CBORTag], Any] | None:
-        return self._tag_hook
-
-    @tag_hook.setter
-    def tag_hook(self, value: Callable[[CBORDecoder, CBORTag], Any] | None) -> None:
-        if value is None or callable(value):
-            self._tag_hook = value
-        else:
-            raise ValueError("tag_hook must be None or a callable")
-
-    @property
-    def object_hook(self) -> Callable[[CBORDecoder, dict[Any, Any]], Any] | None:
-        return self._object_hook
-
-    @object_hook.setter
-    def object_hook(
-        self, value: Callable[[CBORDecoder, Mapping[Any, Any]], Any] | None
-    ) -> None:
-        if value is None or callable(value):
-            self._object_hook = value
-        else:
-            raise ValueError("object_hook must be None or a callable")
-
-    @property
-    def str_errors(self) -> Literal["strict", "error", "replace"]:
-        return self._str_errors
-
-    @str_errors.setter
-    def str_errors(self, value: Literal["strict", "error", "replace"]) -> None:
-        if value in ("strict", "error", "replace"):
-            self._str_errors = value
-        else:
-            raise ValueError(
-                f"invalid str_errors value {value!r} (must be one of 'strict', "
-                "'error', or 'replace')"
-            )
-
-    def set_shareable(self, value: T) -> T:
-        """
-        Set the shareable value for the last encountered shared value marker,
-        if any. If the current shared index is ``None``, nothing is done.
-
-        :param value: the shared value
-        :returns: the shared value to permit chaining
-        """
-        if self._share_index is not None:
-            self._shareables[self._share_index] = value
-
-        return value
-
-    def _stringref_namespace_add(self, string: str | bytes, length: int) -> None:
-        if self._stringref_namespace is not None:
-            next_index = len(self._stringref_namespace)
-            if next_index < 24:
-                is_referenced = length >= 3
-            elif next_index < 256:
-                is_referenced = length >= 4
-            elif next_index < 65536:
-                is_referenced = length >= 5
-            elif next_index < 4294967296:
-                is_referenced = length >= 7
-            else:
-                is_referenced = length >= 11
-
-            if is_referenced:
-                self._stringref_namespace.append(string)
-
-    def read(self, amount: int) -> bytes:
-        """
-        Read bytes from the data stream.
-
-        :param int amount: the number of bytes to read
-        """
-        data = self._fp_read(amount)
-        if len(data) < amount:
-            raise CBORDecodeEOF(
-                f"premature end of stream (expected to read {amount} bytes, got {len(data)} "
-                "instead)"
-            )
-
-        return data
-
-    def _decode(self, immutable: bool = False, unshared: bool = False) -> Any:
-        if immutable:
-            old_immutable = self._immutable
-            self._immutable = True
-        if unshared:
-            old_index = self._share_index
-            self._share_index = None
-        try:
-            initial_byte = self.read(1)[0]
-            major_type = initial_byte >> 5
-            subtype = initial_byte & 31
-            decoder = major_decoders[major_type]
-            return decoder(self, subtype)
-        finally:
-            if immutable:
-                self._immutable = old_immutable
-            if unshared:
-                self._share_index = old_index
-
-    def decode(self) -> object:
-        """
-        Decode the next value from the stream.
-
-        :raises CBORDecodeError: if there is any problem decoding the stream
-        """
-        return self._decode()
-
-    def decode_from_bytes(self, buf: bytes) -> object:
-        """
-        Wrap the given bytestring as a file and call :meth:`decode` with it as
-        the argument.
-
-        This method was intended to be used from the ``tag_hook`` hook when an
-        object needs to be decoded separately from the rest but while still
-        taking advantage of the shared value registry.
-        """
-        with BytesIO(buf) as fp:
-            old_fp = self.fp
-            self.fp = fp
-            retval = self._decode()
-            self.fp = old_fp
-            return retval
-
-    @overload
-    def _decode_length(self, subtype: int) -> int:
-        ...
-
-    @overload
-    def _decode_length(
-        self, subtype: int, allow_indefinite: Literal[True]
-    ) -> int | None:
-        ...
-
-    def _decode_length(
-        self, subtype: int, allow_indefinite: bool = False
-    ) -> int | None:
-        if subtype < 24:
-            return subtype
-        elif subtype == 24:
-            return self.read(1)[0]
-        elif subtype == 25:
-            return cast(int, struct.unpack(">H", self.read(2))[0])
-        elif subtype == 26:
-            return cast(int, struct.unpack(">L", self.read(4))[0])
-        elif subtype == 27:
-            return cast(int, struct.unpack(">Q", self.read(8))[0])
-        elif subtype == 31 and allow_indefinite:
-            return None
-        else:
-            raise CBORDecodeValueError(
-                "unknown unsigned integer subtype 0x%x" % subtype
-            )
-
-    def decode_uint(self, subtype: int) -> int:
-        # Major tag 0
-        return self.set_shareable(self._decode_length(subtype))
-
-    def decode_negint(self, subtype: int) -> int:
-        # Major tag 1
-        return self.set_shareable(-self._decode_length(subtype) - 1)
-
-    def decode_bytestring(self, subtype: int) -> bytes:
-        # Major tag 2
-        length = self._decode_length(subtype, allow_indefinite=True)
-        if length is None:
-            # Indefinite length
-            buf: list[bytes] = []
-            while True:
-                initial_byte = self.read(1)[0]
-                if initial_byte == 0xFF:
-                    result = b"".join(buf)
-                    break
-                elif initial_byte >> 5 == 2:
-                    length = self._decode_length(initial_byte & 0x1F)
-                    if length is None or length > sys.maxsize:
-                        raise CBORDecodeValueError(
-                            "invalid length for indefinite bytestring chunk 0x%x"
-                            % length
-                        )
-                    value = self.read(length)
-                    buf.append(value)
-                else:
-                    raise CBORDecodeValueError(
-                        "non-bytestring found in indefinite length bytestring"
-                    )
-        else:
-            if length > sys.maxsize:
-                raise CBORDecodeValueError(
-                    "invalid length for bytestring 0x%x" % length
-                )
-
-            result = self.read(length)
-            self._stringref_namespace_add(result, length)
-
-        return self.set_shareable(result)
-
-    def decode_string(self, subtype: int) -> str:
-        # Major tag 3
-        length = self._decode_length(subtype, allow_indefinite=True)
-        if length is None:
-            # Indefinite length
-            # NOTE: It may seem redundant to repeat this code to handle UTF-8
-            # strings but there is a reason to do this separately to
-            # byte-strings. Specifically, the CBOR spec states (in sec. 2.2):
-            #
-            #     Text strings with indefinite lengths act the same as byte
-            #     strings with indefinite lengths, except that all their chunks
-            #     MUST be definite-length text strings.  Note that this implies
-            #     that the bytes of a single UTF-8 character cannot be spread
-            #     between chunks: a new chunk can only be started at a
-            #     character boundary.
-            #
-            # This precludes using the indefinite bytestring decoder above as
-            # that would happily ignore UTF-8 characters split across chunks.
-            buf: list[str] = []
-            while True:
-                initial_byte = self.read(1)[0]
-                if initial_byte == 0xFF:
-                    result = "".join(buf)
-                    break
-                elif initial_byte >> 5 == 3:
-                    length = self._decode_length(initial_byte & 0x1F)
-                    if length is None or length > sys.maxsize:
-                        raise CBORDecodeValueError(
-                            "invalid length for indefinite string chunk 0x%x" % length
-                        )
-
-                    value = self.read(length).decode("utf-8", self._str_errors)
-                    buf.append(value)
-                else:
-                    raise CBORDecodeValueError(
-                        "non-string found in indefinite length string"
-                    )
-        else:
-            if length > sys.maxsize:
-                raise CBORDecodeValueError("invalid length for string 0x%x" % length)
-
-            result = self.read(length).decode("utf-8", self._str_errors)
-            self._stringref_namespace_add(result, length)
-
-        return self.set_shareable(result)
-
-    def decode_array(self, subtype: int) -> Sequence[Any]:
-        # Major tag 4
-        length = self._decode_length(subtype, allow_indefinite=True)
-        if length is None:
-            # Indefinite length
-            items: list[Any] = []
-            if not self._immutable:
-                self.set_shareable(items)
-            while True:
-                value = self._decode()
-                if value is break_marker:
-                    break
-                else:
-                    items.append(value)
-        else:
-            if length > sys.maxsize:
-                raise CBORDecodeValueError("invalid length for array 0x%x" % length)
-
-            items = []
-            if not self._immutable:
-                self.set_shareable(items)
-
-            for index in range(length):
-                items.append(self._decode())
-
-        if self._immutable:
-            items_tuple = tuple(items)
-            self.set_shareable(items_tuple)
-            return items_tuple
-
-        return items
-
-    def decode_map(self, subtype: int) -> Mapping[Any, Any]:
-        # Major tag 5
-        length = self._decode_length(subtype, allow_indefinite=True)
-        if length is None:
-            # Indefinite length
-            dictionary: dict[Any, Any] = {}
-            self.set_shareable(dictionary)
-            while True:
-                key = self._decode(immutable=True, unshared=True)
-                if key is break_marker:
-                    break
-                else:
-                    dictionary[key] = self._decode(unshared=True)
-        else:
-            dictionary = {}
-            self.set_shareable(dictionary)
-            for _ in range(length):
-                key = self._decode(immutable=True, unshared=True)
-                dictionary[key] = self._decode(unshared=True)
-
-        if self._object_hook:
-            dictionary = self._object_hook(self, dictionary)
-            self.set_shareable(dictionary)
-        elif self._immutable:
-            frozen_dict = FrozenDict(dictionary)
-            self.set_shareable(dictionary)
-            return frozen_dict
-
-        return dictionary
-
-    def decode_semantic(self, subtype: int) -> Any:
-        # Major tag 6
-        tagnum = self._decode_length(subtype)
-        if semantic_decoder := semantic_decoders.get(tagnum):
-            return semantic_decoder(self)
-
-        tag = CBORTag(tagnum, None)
-        self.set_shareable(tag)
-        tag.value = self._decode(unshared=True)
-        if self._tag_hook:
-            tag = self._tag_hook(self, tag)
-
-        return self.set_shareable(tag)
-
-    def decode_special(self, subtype: int) -> Any:
-        # Simple value
-        if subtype < 20:
-            # XXX Set shareable?
-            return CBORSimpleValue(subtype)
-
-        # Major tag 7
-        try:
-            return special_decoders[subtype](self)
-        except KeyError as e:
-            raise CBORDecodeValueError(
-                "Undefined Reserved major type 7 subtype 0x%x" % subtype
-            ) from e
-
-    #
-    # Semantic decoders (major tag 6)
-    #
-
-    def decode_datetime_string(self) -> datetime:
-        # Semantic tag 0
-        value = self._decode()
-        match = timestamp_re.match(value)
-        if match:
-            (
-                year,
-                month,
-                day,
-                hour,
-                minute,
-                second,
-                secfrac,
-                offset_sign,
-                offset_h,
-                offset_m,
-            ) = match.groups()
-            if secfrac is None:
-                microsecond = 0
-            else:
-                microsecond = int(f"{secfrac:<06}")
-
-            if offset_h:
-                if offset_sign == "-":
-                    sign = -1
-                else:
-                    sign = 1
-                hours = int(offset_h) * sign
-                minutes = int(offset_m) * sign
-                tz = timezone(timedelta(hours=hours, minutes=minutes))
-            else:
-                tz = timezone.utc
-
-            return self.set_shareable(
-                datetime(
-                    int(year),
-                    int(month),
-                    int(day),
-                    int(hour),
-                    int(minute),
-                    int(second),
-                    microsecond,
-                    tz,
-                )
-            )
-        else:
-            raise CBORDecodeValueError(f"invalid datetime string: {value!r}")
-
-    def decode_epoch_datetime(self) -> datetime:
-        # Semantic tag 1
-        value = self._decode()
-        return self.set_shareable(datetime.fromtimestamp(value, timezone.utc))
-
-    def decode_positive_bignum(self) -> int:
-        # Semantic tag 2
-        from binascii import hexlify
-
-        value = self._decode()
-        if not isinstance(value, bytes):
-            raise CBORDecodeValueError("invalid bignum value " + str(value))
-
-        return self.set_shareable(int(hexlify(value), 16))
-
-    def decode_negative_bignum(self) -> int:
-        # Semantic tag 3
-        return self.set_shareable(-self.decode_positive_bignum() - 1)
-
-    def decode_fraction(self) -> Decimal:
-        # Semantic tag 4
-        from decimal import Decimal
-
-        try:
-            exp, sig = self._decode()
-        except (TypeError, ValueError) as e:
-            raise CBORDecodeValueError("Incorrect tag 4 payload") from e
-        tmp = Decimal(sig).as_tuple()
-        return self.set_shareable(Decimal((tmp.sign, tmp.digits, exp)))
-
-    def decode_bigfloat(self) -> Decimal:
-        # Semantic tag 5
-        from decimal import Decimal
-
-        try:
-            exp, sig = self._decode()
-        except (TypeError, ValueError) as e:
-            raise CBORDecodeValueError("Incorrect tag 5 payload") from e
-
-        return self.set_shareable(Decimal(sig) * (2 ** Decimal(exp)))
-
-    def decode_stringref(self) -> str | bytes:
-        # Semantic tag 25
-        if self._stringref_namespace is None:
-            raise CBORDecodeValueError("string reference outside of namespace")
-
-        index: int = self._decode()
-        try:
-            value = self._stringref_namespace[index]
-        except IndexError:
-            raise CBORDecodeValueError("string reference %d not found" % index)
-
-        return value
-
-    def decode_shareable(self) -> object:
-        # Semantic tag 28
-        old_index = self._share_index
-        self._share_index = len(self._shareables)
-        self._shareables.append(None)
-        try:
-            return self._decode()
-        finally:
-            self._share_index = old_index
-
-    def decode_sharedref(self) -> Any:
-        # Semantic tag 29
-        value = self._decode(unshared=True)
-        try:
-            shared = self._shareables[value]
-        except IndexError:
-            raise CBORDecodeValueError("shared reference %d not found" % value)
-
-        if shared is None:
-            raise CBORDecodeValueError(
-                "shared value %d has not been initialized" % value
-            )
-        else:
-            return shared
-
-    def decode_rational(self) -> Fraction:
-        # Semantic tag 30
-        from fractions import Fraction
-
-        return self.set_shareable(Fraction(*self._decode()))
-
-    def decode_regexp(self) -> re.Pattern[str]:
-        # Semantic tag 35
-        return self.set_shareable(re.compile(self._decode()))
-
-    def decode_mime(self) -> Message:
-        # Semantic tag 36
-        from email.parser import Parser
-
-        return self.set_shareable(Parser().parsestr(self._decode()))
-
-    def decode_uuid(self) -> UUID:
-        # Semantic tag 37
-        from uuid import UUID
-
-        return self.set_shareable(UUID(bytes=self._decode()))
-
-    def decode_stringref_namespace(self) -> Any:
-        # Semantic tag 256
-        old_namespace = self._stringref_namespace
-        self._stringref_namespace = []
-        value = self._decode()
-        self._stringref_namespace = old_namespace
-        return value
-
-    def decode_set(self) -> set[Any] | frozenset[Any]:
-        # Semantic tag 258
-        if self._immutable:
-            return self.set_shareable(frozenset(self._decode(immutable=True)))
-        else:
-            return self.set_shareable(set(self._decode(immutable=True)))
-
-    def decode_ipaddress(self) -> IPv4Address | IPv6Address | CBORTag:
-        # Semantic tag 260
-        from ipaddress import ip_address
-
-        buf = self.decode()
-        if not isinstance(buf, bytes) or len(buf) not in (4, 6, 16):
-            raise CBORDecodeValueError(f"invalid ipaddress value {buf!r}")
-        elif len(buf) in (4, 16):
-            return self.set_shareable(ip_address(buf))
-        elif len(buf) == 6:
-            # MAC address
-            return self.set_shareable(CBORTag(260, buf))
-
-        raise CBORDecodeValueError(f"invalid ipaddress value {buf!r}")
-
-    def decode_ipnetwork(self) -> IPv4Network | IPv6Network:
-        # Semantic tag 261
-        from ipaddress import ip_network
-
-        net_map = self.decode()
-        if isinstance(net_map, Mapping) and len(net_map) == 1:
-            for net in net_map.items():
-                try:
-                    return self.set_shareable(ip_network(net, strict=False))
-                except (TypeError, ValueError):
-                    break
-
-        raise CBORDecodeValueError(f"invalid ipnetwork value {net_map!r}")
-
-    def decode_self_describe_cbor(self) -> Any:
-        # Semantic tag 55799
-        return self._decode()
-
-    #
-    # Special decoders (major tag 7)
-    #
-
-    def decode_simple_value(self) -> CBORSimpleValue:
-        # XXX Set shareable?
-        return CBORSimpleValue(self.read(1)[0])
-
-    def decode_float16(self) -> float:
-        return self.set_shareable(cast(float, struct.unpack(">e", self.read(2))[0]))
-
-    def decode_float32(self) -> float:
-        return self.set_shareable(cast(float, struct.unpack(">f", self.read(4))[0]))
-
-    def decode_float64(self) -> float:
-        return self.set_shareable(cast(float, struct.unpack(">d", self.read(8))[0]))
-
-
-major_decoders: dict[int, Callable[[CBORDecoder, int], Any]] = {
-    0: CBORDecoder.decode_uint,
-    1: CBORDecoder.decode_negint,
-    2: CBORDecoder.decode_bytestring,
-    3: CBORDecoder.decode_string,
-    4: CBORDecoder.decode_array,
-    5: CBORDecoder.decode_map,
-    6: CBORDecoder.decode_semantic,
-    7: CBORDecoder.decode_special,
-}
-
-special_decoders: dict[int, Callable[[CBORDecoder], Any]] = {
-    20: lambda self: False,
-    21: lambda self: True,
-    22: lambda self: None,
-    23: lambda self: undefined,
-    24: CBORDecoder.decode_simple_value,
-    25: CBORDecoder.decode_float16,
-    26: CBORDecoder.decode_float32,
-    27: CBORDecoder.decode_float64,
-    31: lambda self: break_marker,
-}
-
-semantic_decoders: dict[int, Callable[[CBORDecoder], Any]] = {
-    0: CBORDecoder.decode_datetime_string,
-    1: CBORDecoder.decode_epoch_datetime,
-    2: CBORDecoder.decode_positive_bignum,
-    3: CBORDecoder.decode_negative_bignum,
-    4: CBORDecoder.decode_fraction,
-    5: CBORDecoder.decode_bigfloat,
-    25: CBORDecoder.decode_stringref,
-    28: CBORDecoder.decode_shareable,
-    29: CBORDecoder.decode_sharedref,
-    30: CBORDecoder.decode_rational,
-    35: CBORDecoder.decode_regexp,
-    36: CBORDecoder.decode_mime,
-    37: CBORDecoder.decode_uuid,
-    256: CBORDecoder.decode_stringref_namespace,
-    258: CBORDecoder.decode_set,
-    260: CBORDecoder.decode_ipaddress,
-    261: CBORDecoder.decode_ipnetwork,
-    55799: CBORDecoder.decode_self_describe_cbor,
-}
-
-
-def loads(
-    s: bytes | bytearray | memoryview,
-    tag_hook: Callable[[CBORDecoder, CBORTag], Any] | None = None,
-    object_hook: Callable[[CBORDecoder, dict[Any, Any]], Any] | None = None,
-    str_errors: Literal["strict", "error", "replace"] = "strict",
-) -> object:
-    """
-    Deserialize an object from a bytestring.
-
-    :param bytes s:
-        the bytestring to deserialize
-    :param tag_hook:
-        callable that takes 2 arguments: the decoder instance, and the :class:`.CBORTag`
-        to be decoded. This callback is invoked for any tags for which there is no
-        built-in decoder. The return value is substituted for the :class:`.CBORTag`
-        object in the deserialized output
-    :param object_hook:
-        callable that takes 2 arguments: the decoder instance, and a dictionary. This
-        callback is invoked for each deserialized :class:`dict` object. The return value
-        is substituted for the dict in the deserialized output.
-    :param str_errors:
-        determines how to handle unicode decoding errors (see the `Error Handlers`_
-        section in the standard library documentation for details)
-    :return:
-        the deserialized object
-
-    .. _Error Handlers: https://docs.python.org/3/library/codecs.html#error-handlers
-
-    """
-    with BytesIO(s) as fp:
-        return CBORDecoder(
-            fp, tag_hook=tag_hook, object_hook=object_hook, str_errors=str_errors
-        ).decode()
-
-
-def load(
-    fp: BytesIO,
-    tag_hook: Callable[[CBORDecoder, CBORTag], Any] | None = None,
-    object_hook: Callable[[CBORDecoder, dict[Any, Any]], Any] | None = None,
-    str_errors: Literal["strict", "error", "replace"] = "strict",
-) -> object:
-    """
-    Deserialize an object from an open file.
-
-    :param fp:
-        the file to read from (any file-like object opened for reading in binary mode)
-    :param tag_hook:
-        callable that takes 2 arguments: the decoder instance, and the :class:`.CBORTag`
-        to be decoded. This callback is invoked for any tags for which there is no
-        built-in decoder. The return value is substituted for the :class:`.CBORTag`
-        object in the deserialized output
-    :param object_hook:
-        callable that takes 2 arguments: the decoder instance, and a dictionary. This
-        callback is invoked for each deserialized :class:`dict` object. The return value
-        is substituted for the dict in the deserialized output.
-    :param str_errors:
-        determines how to handle unicode decoding errors (see the `Error Handlers`_
-        section in the standard library documentation for details)
-    :return:
-        the deserialized object
-
-    .. _Error Handlers: https://docs.python.org/3/library/codecs.html#error-handlers
-
-    """
-    return CBORDecoder(
-        fp, tag_hook=tag_hook, object_hook=object_hook, str_errors=str_errors
-    ).decode()
diff --git a/cbor2/encoder.py b/cbor2/encoder.py
index 2c110433..92ead947 100644
--- a/cbor2/encoder.py
+++ b/cbor2/encoder.py
@@ -1,811 +1,10 @@
-from __future__ import annotations
+from warnings import warn
 
-import math
-import re
-import struct
-import sys
-from collections import OrderedDict, defaultdict
-from collections.abc import Callable, Generator, Mapping, Sequence, Set
-from contextlib import contextmanager
-from datetime import date, datetime, time, tzinfo
-from functools import wraps
-from io import BytesIO
-from sys import modules
-from typing import TYPE_CHECKING, Any, cast
+from ._encoder import CBOREncoder as CBOREncoder
+from ._encoder import dump as dump
+from ._encoder import dumps as dumps
+from ._encoder import shareable_encoder as shareable_encoder
 
-from .types import (
-    CBOREncodeTypeError,
-    CBOREncodeValueError,
-    CBORSimpleValue,
-    CBORTag,
-    FrozenDict,
-    UndefinedType,
-    undefined,
+warn(
+    "The cbor2.encoder module has been deprecated. Instead import everything directly from cbor2."
 )
-
-if TYPE_CHECKING:
-    from decimal import Decimal
-    from email.message import Message
-    from fractions import Fraction
-    from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
-    from uuid import UUID
-
-    if sys.version_info >= (3, 12):
-        from typing import Buffer
-    else:
-        from typing_extensions import Buffer
-
-
-def shareable_encoder(
-    func: Callable[[CBOREncoder, Any], None]
-) -> Callable[[CBOREncoder, Any], None]:
-    """
-    Wrap the given encoder function to gracefully handle cyclic data
-    structures.
-
-    If value sharing is enabled, this marks the given value shared in the
-    datastream on the first call. If the value has already been passed to this
-    method, a reference marker is instead written to the data stream and the
-    wrapped function is not called.
-
-    If value sharing is disabled, only infinite recursion protection is done.
-    """
-
-    @wraps(func)
-    def wrapper(encoder: CBOREncoder, value: Any) -> None:
-        encoder.encode_shared(func, value)
-
-    return wrapper
-
-
-def container_encoder(
-    func: Callable[[CBOREncoder, Any], Any]
-) -> Callable[[CBOREncoder, Any], Any]:
-    """
-    The given encoder is a container with child values. Handle cyclic or
-    duplicate references to the value and strings within the value
-    efficiently.
-
-    Containers may contain cyclic data structures or may contain values
-    or themselves by referenced multiple times throughout the greater
-    encoded value and could thus be more efficiently encoded with shared
-    value references and string references where duplication occurs.
-
-    If value sharing is enabled, this marks the given value shared in the
-    datastream on the first call. If the value has already been passed to this
-    method, a reference marker is instead written to the data stream and the
-    wrapped function is not called.
-
-    If value sharing is disabled, only infinite recursion protection is done.
-
-    If string referencing is enabled and this is the first use of this
-    method in encoding a value, all repeated references to long strings
-    and bytearrays will be replaced with references to the first
-    occurrence of those arrays.
-
-    If string referencing is disabled, all strings and bytearrays will
-    be encoded directly.
-    """
-
-    @wraps(func)
-    def wrapper(encoder: CBOREncoder, value: Any) -> None:
-        encoder.encode_container(func, value)
-
-    return wrapper
-
-
-class CBOREncoder:
-    """
-    The CBOREncoder class implements a fully featured `CBOR`_ encoder with
-    several extensions for handling shared references, big integers, rational
-    numbers and so on. Typically the class is not used directly, but the
-    :func:`dump` and :func:`dumps` functions are called to indirectly construct
-    and use the class.
-
-    When the class is constructed manually, the main entry points are
-    :meth:`encode` and :meth:`encode_to_bytes`.
-
-    .. _CBOR: https://cbor.io/
-    """
-
-    __slots__ = (
-        "datetime_as_timestamp",
-        "_timezone",
-        "_default",
-        "value_sharing",
-        "_fp",
-        "_fp_write",
-        "_shared_containers",
-        "_encoders",
-        "_canonical",
-        "string_referencing",
-        "string_namespacing",
-        "_string_references",
-    )
-
-    _fp: BytesIO
-    _fp_write: Callable[[Buffer], int]
-
-    def __init__(
-        self,
-        fp: BytesIO,
-        datetime_as_timestamp: bool = False,
-        timezone: tzinfo | None = None,
-        value_sharing: bool = False,
-        default: Callable[[CBOREncoder, Any], Any] | None = None,
-        canonical: bool = False,
-        date_as_datetime: bool = False,
-        string_referencing: bool = False,
-    ):
-        """
-        :param fp:
-            the file to write to (any file-like object opened for writing in binary
-            mode)
-        :param datetime_as_timestamp:
-            set to ``True`` to serialize datetimes as UNIX timestamps (this makes
-            datetimes more concise on the wire, but loses the timezone information)
-        :param timezone:
-            the default timezone to use for serializing naive datetimes; if this is not
-            specified naive datetimes will throw a :exc:`ValueError` when encoding is
-            attempted
-        :param value_sharing:
-            set to ``True`` to allow more efficient serializing of repeated values and,
-            more importantly, cyclic data structures, at the cost of extra line overhead
-        :param default:
-            a callable that is called by the encoder with two arguments (the encoder
-            instance and the value being encoded) when no suitable encoder has been
-            found, and should use the methods on the encoder to encode any objects it
-            wants to add to the data stream
-        :param canonical:
-            when ``True``, use "canonical" CBOR representation; this typically involves
-            sorting maps, sets, etc. into a pre-determined order ensuring that
-            serializations are comparable without decoding
-        :param date_as_datetime:
-            set to ``True`` to serialize date objects as datetimes (CBOR tag 0), which
-            was the default behavior in previous releases (cbor2 <= 4.1.2).
-        :param string_referencing:
-            set to ``True`` to allow more efficient serializing of repeated string
-            values
-
-        """
-        self.fp = fp
-        self.datetime_as_timestamp = datetime_as_timestamp
-        self.timezone = timezone
-        self.value_sharing = value_sharing
-        self.string_referencing = string_referencing
-        self.string_namespacing = string_referencing
-        self.default = default
-        self._canonical = canonical
-        self._shared_containers: dict[
-            int, tuple[object, int | None]
-        ] = {}  # indexes used for value sharing
-        self._string_references: dict[
-            str | bytes, int
-        ] = {}  # indexes used for string references
-        self._encoders = default_encoders.copy()
-        if canonical:
-            self._encoders.update(canonical_encoders)
-        if date_as_datetime:
-            self._encoders[date] = CBOREncoder.encode_date
-
-    def _find_encoder(
-        self, obj_type: type
-    ) -> Callable[[CBOREncoder, Any], None] | None:
-        for type_or_tuple, enc in list(self._encoders.items()):
-            if type(type_or_tuple) is tuple:
-                try:
-                    modname, typename = type_or_tuple
-                except (TypeError, ValueError):
-                    raise CBOREncodeValueError(
-                        f"invalid deferred encoder type {type_or_tuple!r} (must be a "
-                        "2-tuple of module name and type name, e.g. "
-                        "('collections', 'defaultdict'))"
-                    )
-
-                imported_type = getattr(modules.get(modname), typename, None)
-                if imported_type is not None:
-                    del self._encoders[type_or_tuple]
-                    self._encoders[imported_type] = enc
-                    type_ = imported_type
-                else:  # pragma: nocover
-                    continue
-            else:
-                type_ = type_or_tuple
-
-            if issubclass(obj_type, type_):
-                self._encoders[obj_type] = enc
-                return enc
-
-        return None
-
-    @property
-    def fp(self) -> BytesIO:
-        return self._fp
-
-    @fp.setter
-    def fp(self, value: BytesIO) -> None:
-        try:
-            if not callable(value.write):
-                raise ValueError("fp.write is not callable")
-        except AttributeError:
-            raise ValueError("fp object has no write method")
-        else:
-            self._fp = value
-            self._fp_write = value.write
-
-    @property
-    def timezone(self) -> tzinfo | None:
-        return self._timezone
-
-    @timezone.setter
-    def timezone(self, value: tzinfo | None) -> None:
-        if value is None or isinstance(value, tzinfo):
-            self._timezone = value
-        else:
-            raise ValueError("timezone must be None or a tzinfo instance")
-
-    @property
-    def default(self) -> Callable[[CBOREncoder, Any], Any] | None:
-        return self._default
-
-    @default.setter
-    def default(self, value: Callable[[CBOREncoder, Any], Any] | None) -> None:
-        if value is None or callable(value):
-            self._default = value
-        else:
-            raise ValueError("default must be None or a callable")
-
-    @property
-    def canonical(self) -> bool:
-        return self._canonical
-
-    @contextmanager
-    def disable_value_sharing(self) -> Generator[None, None, None]:
-        """
-        Disable value sharing in the encoder for the duration of the context
-        block.
-        """
-        old_value_sharing = self.value_sharing
-        self.value_sharing = False
-        yield
-        self.value_sharing = old_value_sharing
-
-    @contextmanager
-    def disable_string_referencing(self) -> Generator[None, None, None]:
-        """
-        Disable tracking of string references for the duration of the
-        context block.
-        """
-        old_string_referencing = self.string_referencing
-        self.string_referencing = False
-        yield
-        self.string_referencing = old_string_referencing
-
-    @contextmanager
-    def disable_string_namespacing(self) -> Generator[None, None, None]:
-        """
-        Disable generation of new string namespaces for the duration of the
-        context block.
-        """
-        old_string_namespacing = self.string_namespacing
-        self.string_namespacing = False
-        yield
-        self.string_namespacing = old_string_namespacing
-
-    def write(self, data: bytes) -> None:
-        """
-        Write bytes to the data stream.
-
-        :param bytes data:
-            the bytes to write
-        """
-        self._fp_write(data)
-
-    def encode(self, obj: Any) -> None:
-        """
-        Encode the given object using CBOR.
-
-        :param obj:
-            the object to encode
-        """
-        obj_type = obj.__class__
-        encoder = (
-            self._encoders.get(obj_type)
-            or self._find_encoder(obj_type)
-            or self._default
-        )
-        if not encoder:
-            raise CBOREncodeTypeError("cannot serialize type %s" % obj_type.__name__)
-
-        encoder(self, obj)
-
-    def encode_to_bytes(self, obj: Any) -> bytes:
-        """
-        Encode the given object to a byte buffer and return its value as bytes.
-
-        This method was intended to be used from the ``default`` hook when an
-        object needs to be encoded separately from the rest but while still
-        taking advantage of the shared value registry.
-        """
-        with BytesIO() as fp:
-            old_fp = self.fp
-            self.fp = fp
-            self.encode(obj)
-            self.fp = old_fp
-            return fp.getvalue()
-
-    def encode_container(
-        self, encoder: Callable[[CBOREncoder, Any], Any], value: Any
-    ) -> None:
-        if self.string_namespacing:
-            # Create a new string reference domain
-            self.encode_length(6, 256)
-
-        with self.disable_string_namespacing():
-            self.encode_shared(encoder, value)
-
-    def encode_shared(
-        self, encoder: Callable[[CBOREncoder, Any], Any], value: Any
-    ) -> None:
-        value_id = id(value)
-        try:
-            index = self._shared_containers[id(value)][1]
-        except KeyError:
-            if self.value_sharing:
-                # Mark the container as shareable
-                self._shared_containers[value_id] = (
-                    value,
-                    len(self._shared_containers),
-                )
-                self.encode_length(6, 0x1C)
-                encoder(self, value)
-            else:
-                self._shared_containers[value_id] = (value, None)
-                try:
-                    encoder(self, value)
-                finally:
-                    del self._shared_containers[value_id]
-        else:
-            if self.value_sharing:
-                # Generate a reference to the previous index instead of
-                # encoding this again
-                self.encode_length(6, 0x1D)
-                self.encode_int(cast(int, index))
-            else:
-                raise CBOREncodeValueError(
-                    "cyclic data structure detected but value sharing is disabled"
-                )
-
-    def _stringref(self, value: str | bytes) -> bool:
-        """
-        Try to encode the string or bytestring as a reference.
-
-        Returns True if a reference was generated, False if the string
-        must still be emitted.
-        """
-        try:
-            index = self._string_references[value]
-            self.encode_semantic(CBORTag(25, index))
-            return True
-        except KeyError:
-            length = len(value)
-            next_index = len(self._string_references)
-            if next_index < 24:
-                is_referenced = length >= 3
-            elif next_index < 256:
-                is_referenced = length >= 4
-            elif next_index < 65536:
-                is_referenced = length >= 5
-            elif next_index < 4294967296:
-                is_referenced = length >= 7
-            else:
-                is_referenced = length >= 11
-
-            if is_referenced:
-                self._string_references[value] = next_index
-
-            return False
-
-    def encode_length(self, major_tag: int, length: int) -> None:
-        major_tag <<= 5
-        if length < 24:
-            self._fp_write(struct.pack(">B", major_tag | length))
-        elif length < 256:
-            self._fp_write(struct.pack(">BB", major_tag | 24, length))
-        elif length < 65536:
-            self._fp_write(struct.pack(">BH", major_tag | 25, length))
-        elif length < 4294967296:
-            self._fp_write(struct.pack(">BL", major_tag | 26, length))
-        else:
-            self._fp_write(struct.pack(">BQ", major_tag | 27, length))
-
-    def encode_int(self, value: int) -> None:
-        # Big integers (2 ** 64 and over)
-        if value >= 18446744073709551616 or value < -18446744073709551616:
-            if value >= 0:
-                major_type = 0x02
-            else:
-                major_type = 0x03
-                value = -value - 1
-
-            payload = value.to_bytes((value.bit_length() + 7) // 8, "big")
-            self.encode_semantic(CBORTag(major_type, payload))
-        elif value >= 0:
-            self.encode_length(0, value)
-        else:
-            self.encode_length(1, -(value + 1))
-
-    def encode_bytestring(self, value: bytes) -> None:
-        if self.string_referencing:
-            if self._stringref(value):
-                return
-
-        self.encode_length(2, len(value))
-        self._fp_write(value)
-
-    def encode_bytearray(self, value: bytearray) -> None:
-        self.encode_bytestring(bytes(value))
-
-    def encode_string(self, value: str) -> None:
-        if self.string_referencing:
-            if self._stringref(value):
-                return
-
-        encoded = value.encode("utf-8")
-        self.encode_length(3, len(encoded))
-        self._fp_write(encoded)
-
-    @container_encoder
-    def encode_array(self, value: Sequence[Any]) -> None:
-        self.encode_length(4, len(value))
-        for item in value:
-            self.encode(item)
-
-    @container_encoder
-    def encode_map(self, value: Mapping[Any, Any]) -> None:
-        self.encode_length(5, len(value))
-        for key, val in value.items():
-            self.encode(key)
-            self.encode(val)
-
-    def encode_sortable_key(self, value: Any) -> tuple[int, bytes]:
-        """
-        Takes a key and calculates the length of its optimal byte
-        representation, along with the representation itself. This is used as
-        the sorting key in CBOR's canonical representations.
-        """
-        with self.disable_string_referencing():
-            encoded = self.encode_to_bytes(value)
-            return len(encoded), encoded
-
-    @container_encoder
-    def encode_canonical_map(self, value: Mapping[Any, Any]) -> None:
-        """Reorder keys according to Canonical CBOR specification"""
-        keyed_keys = (
-            (self.encode_sortable_key(key), key, value) for key, value in value.items()
-        )
-        self.encode_length(5, len(value))
-        for sortkey, realkey, value in sorted(keyed_keys):
-            if self.string_referencing:
-                # String referencing requires that the order encoded is
-                # the same as the order emitted so string references are
-                # generated after an order is determined
-                self.encode(realkey)
-            else:
-                self._fp_write(sortkey[1])
-            self.encode(value)
-
-    def encode_semantic(self, value: CBORTag) -> None:
-        # Nested string reference domains are distinct
-        old_string_referencing = self.string_referencing
-        old_string_references = self._string_references
-        if value.tag == 256:
-            self.string_referencing = True
-            self._string_references = {}
-
-        self.encode_length(6, value.tag)
-        self.encode(value.value)
-
-        self.string_referencing = old_string_referencing
-        self._string_references = old_string_references
-
-    #
-    # Semantic decoders (major tag 6)
-    #
-
-    def encode_datetime(self, value: datetime) -> None:
-        # Semantic tag 0
-        if not value.tzinfo:
-            if self._timezone:
-                value = value.replace(tzinfo=self._timezone)
-            else:
-                raise CBOREncodeValueError(
-                    f"naive datetime {value!r} encountered and no default timezone "
-                    "has been set"
-                )
-
-        if self.datetime_as_timestamp:
-            from calendar import timegm
-
-            if not value.microsecond:
-                timestamp: float = timegm(value.utctimetuple())
-            else:
-                timestamp = timegm(value.utctimetuple()) + value.microsecond / 1000000
-
-            self.encode_semantic(CBORTag(1, timestamp))
-        else:
-            datestring = value.isoformat().replace("+00:00", "Z")
-            self.encode_semantic(CBORTag(0, datestring))
-
-    def encode_date(self, value: date) -> None:
-        value = datetime.combine(value, time()).replace(tzinfo=self._timezone)
-        self.encode_datetime(value)
-
-    def encode_decimal(self, value: Decimal) -> None:
-        # Semantic tag 4
-        if value.is_nan():
-            self._fp_write(b"\xf9\x7e\x00")
-        elif value.is_infinite():
-            self._fp_write(b"\xf9\x7c\x00" if value > 0 else b"\xf9\xfc\x00")
-        else:
-            dt = value.as_tuple()
-            sig = 0
-            for digit in dt.digits:
-                sig = (sig * 10) + digit
-            if dt.sign:
-                sig = -sig
-            with self.disable_value_sharing():
-                self.encode_semantic(CBORTag(4, [dt.exponent, sig]))
-
-    def encode_stringref(self, value: str | bytes) -> None:
-        # Semantic tag 25
-        if not self._stringref(value):
-            self.encode(value)
-
-    def encode_rational(self, value: Fraction) -> None:
-        # Semantic tag 30
-        with self.disable_value_sharing():
-            self.encode_semantic(CBORTag(30, [value.numerator, value.denominator]))
-
-    def encode_regexp(self, value: re.Pattern[str]) -> None:
-        # Semantic tag 35
-        self.encode_semantic(CBORTag(35, str(value.pattern)))
-
-    def encode_mime(self, value: Message) -> None:
-        # Semantic tag 36
-        self.encode_semantic(CBORTag(36, value.as_string()))
-
-    def encode_uuid(self, value: UUID) -> None:
-        # Semantic tag 37
-        self.encode_semantic(CBORTag(37, value.bytes))
-
-    def encode_stringref_namespace(self, value: Any) -> None:
-        # Semantic tag 256
-        with self.disable_string_namespacing():
-            self.encode_semantic(CBORTag(256, value))
-
-    def encode_set(self, value: Set[Any]) -> None:
-        # Semantic tag 258
-        self.encode_semantic(CBORTag(258, tuple(value)))
-
-    def encode_canonical_set(self, value: Set[Any]) -> None:
-        # Semantic tag 258
-        values = sorted((self.encode_sortable_key(key), key) for key in value)
-        self.encode_semantic(CBORTag(258, [key[1] for key in values]))
-
-    def encode_ipaddress(self, value: IPv4Address | IPv6Address) -> None:
-        # Semantic tag 260
-        self.encode_semantic(CBORTag(260, value.packed))
-
-    def encode_ipnetwork(self, value: IPv4Network | IPv6Network) -> None:
-        # Semantic tag 261
-        self.encode_semantic(
-            CBORTag(261, {value.network_address.packed: value.prefixlen})
-        )
-
-    #
-    # Special encoders (major tag 7)
-    #
-
-    def encode_simple_value(self, value: CBORSimpleValue) -> None:
-        if value.value < 20:
-            self._fp_write(struct.pack(">B", 0xE0 | value.value))
-        else:
-            self._fp_write(struct.pack(">BB", 0xF8, value.value))
-
-    def encode_float(self, value: float) -> None:
-        # Handle special values efficiently
-        if math.isnan(value):
-            self._fp_write(b"\xf9\x7e\x00")
-        elif math.isinf(value):
-            self._fp_write(b"\xf9\x7c\x00" if value > 0 else b"\xf9\xfc\x00")
-        else:
-            self._fp_write(struct.pack(">Bd", 0xFB, value))
-
-    def encode_minimal_float(self, value: float) -> None:
-        # Handle special values efficiently
-        if math.isnan(value):
-            self._fp_write(b"\xf9\x7e\x00")
-        elif math.isinf(value):
-            self._fp_write(b"\xf9\x7c\x00" if value > 0 else b"\xf9\xfc\x00")
-        else:
-            # Try each encoding in turn from longest to shortest
-            encoded = struct.pack(">Bd", 0xFB, value)
-            for format, tag in [(">Bf", 0xFA), (">Be", 0xF9)]:
-                try:
-                    new_encoded = struct.pack(format, tag, value)
-                    # Check if encoding as low-byte float loses precision
-                    if struct.unpack(format, new_encoded)[1] == value:
-                        encoded = new_encoded
-                    else:
-                        break
-                except OverflowError:
-                    break
-
-            self._fp_write(encoded)
-
-    def encode_boolean(self, value: bool) -> None:
-        self._fp_write(b"\xf5" if value else b"\xf4")
-
-    def encode_none(self, value: None) -> None:
-        self._fp_write(b"\xf6")
-
-    def encode_undefined(self, value: UndefinedType) -> None:
-        self._fp_write(b"\xf7")
-
-
-default_encoders: dict[type | tuple[str, str], Callable[[CBOREncoder, Any], None]] = {
-    bytes: CBOREncoder.encode_bytestring,
-    bytearray: CBOREncoder.encode_bytearray,
-    str: CBOREncoder.encode_string,
-    int: CBOREncoder.encode_int,
-    float: CBOREncoder.encode_float,
-    ("decimal", "Decimal"): CBOREncoder.encode_decimal,
-    bool: CBOREncoder.encode_boolean,
-    type(None): CBOREncoder.encode_none,
-    tuple: CBOREncoder.encode_array,
-    list: CBOREncoder.encode_array,
-    dict: CBOREncoder.encode_map,
-    defaultdict: CBOREncoder.encode_map,
-    OrderedDict: CBOREncoder.encode_map,
-    FrozenDict: CBOREncoder.encode_map,
-    type(undefined): CBOREncoder.encode_undefined,
-    datetime: CBOREncoder.encode_datetime,
-    re.Pattern: CBOREncoder.encode_regexp,
-    ("fractions", "Fraction"): CBOREncoder.encode_rational,
-    ("email.message", "Message"): CBOREncoder.encode_mime,
-    ("uuid", "UUID"): CBOREncoder.encode_uuid,
-    ("ipaddress", "IPv4Address"): CBOREncoder.encode_ipaddress,
-    ("ipaddress", "IPv6Address"): CBOREncoder.encode_ipaddress,
-    ("ipaddress", "IPv4Network"): CBOREncoder.encode_ipnetwork,
-    ("ipaddress", "IPv6Network"): CBOREncoder.encode_ipnetwork,
-    CBORSimpleValue: CBOREncoder.encode_simple_value,
-    CBORTag: CBOREncoder.encode_semantic,
-    set: CBOREncoder.encode_set,
-    frozenset: CBOREncoder.encode_set,
-}
-
-
-canonical_encoders: dict[type | tuple[str, str], Callable[[CBOREncoder, Any], None]] = {
-    float: CBOREncoder.encode_minimal_float,
-    dict: CBOREncoder.encode_canonical_map,
-    defaultdict: CBOREncoder.encode_canonical_map,
-    OrderedDict: CBOREncoder.encode_canonical_map,
-    FrozenDict: CBOREncoder.encode_canonical_map,
-    set: CBOREncoder.encode_canonical_set,
-    frozenset: CBOREncoder.encode_canonical_set,
-}
-
-
-def dumps(
-    obj: object,
-    datetime_as_timestamp: bool = False,
-    timezone: tzinfo | None = None,
-    value_sharing: bool = False,
-    default: Callable[[CBOREncoder, Any], None] | None = None,
-    canonical: bool = False,
-    date_as_datetime: bool = False,
-    string_referencing: bool = False,
-) -> bytes:
-    """
-    Serialize an object to a bytestring.
-
-    :param obj:
-        the object to serialize
-    :param datetime_as_timestamp:
-        set to ``True`` to serialize datetimes as UNIX timestamps (this makes datetimes
-        more concise on the wire, but loses the timezone information)
-    :param timezone:
-        the default timezone to use for serializing naive datetimes; if this is not
-        specified naive datetimes will throw a :exc:`ValueError` when encoding is
-        attempted
-    :param value_sharing:
-        set to ``True`` to allow more efficient serializing of repeated values
-        and, more importantly, cyclic data structures, at the cost of extra
-        line overhead
-    :param default:
-        a callable that is called by the encoder with two arguments (the encoder
-        instance and the value being encoded) when no suitable encoder has been found,
-        and should use the methods on the encoder to encode any objects it wants to add
-        to the data stream
-    :param canonical:
-        when ``True``, use "canonical" CBOR representation; this typically involves
-        sorting maps, sets, etc. into a pre-determined order ensuring that
-        serializations are comparable without decoding
-    :param date_as_datetime:
-        set to ``True`` to serialize date objects as datetimes (CBOR tag 0), which was
-        the default behavior in previous releases (cbor2 <= 4.1.2).
-    :param string_referencing:
-        set to ``True`` to allow more efficient serializing of repeated string values
-    :return: the serialized output
-
-    """
-    with BytesIO() as fp:
-        CBOREncoder(
-            fp,
-            datetime_as_timestamp=datetime_as_timestamp,
-            timezone=timezone,
-            value_sharing=value_sharing,
-            default=default,
-            canonical=canonical,
-            date_as_datetime=date_as_datetime,
-            string_referencing=string_referencing,
-        ).encode(obj)
-        return fp.getvalue()
-
-
-def dump(
-    obj: object,
-    fp: BytesIO,
-    datetime_as_timestamp: bool = False,
-    timezone: tzinfo | None = None,
-    value_sharing: bool = False,
-    default: Callable[[CBOREncoder, Any], None] | None = None,
-    canonical: bool = False,
-    date_as_datetime: bool = False,
-    string_referencing: bool = False,
-) -> None:
-    """
-    Serialize an object to a file.
-
-    :param obj:
-        the object to serialize
-    :param fp:
-        the file to write to (any file-like object opened for writing in binary mode)
-    :param datetime_as_timestamp:
-        set to ``True`` to serialize datetimes as UNIX timestamps (this makes datetimes
-        more concise on the wire, but loses the timezone information)
-    :param timezone:
-        the default timezone to use for serializing naive datetimes; if this is not
-        specified naive datetimes will throw a :exc:`ValueError` when encoding is
-        attempted
-    :param value_sharing:
-        set to ``True`` to allow more efficient serializing of repeated values
-        and, more importantly, cyclic data structures, at the cost of extra
-        line overhead
-    :param default:
-        a callable that is called by the encoder with two arguments (the encoder
-        instance and the value being encoded) when no suitable encoder has been found,
-        and should use the methods on the encoder to encode any objects it wants to add
-        to the data stream
-    :param canonical:
-        when ``True``, use "canonical" CBOR representation; this typically involves
-        sorting maps, sets, etc. into a pre-determined order ensuring that
-        serializations are comparable without decoding
-    :param date_as_datetime:
-        set to ``True`` to serialize date objects as datetimes (CBOR tag 0), which was
-        the default behavior in previous releases (cbor2 <= 4.1.2).
-    :param string_referencing:
-        set to ``True`` to allow more efficient serializing of repeated string values
-
-    """
-    CBOREncoder(
-        fp,
-        datetime_as_timestamp=datetime_as_timestamp,
-        timezone=timezone,
-        value_sharing=value_sharing,
-        default=default,
-        canonical=canonical,
-        date_as_datetime=date_as_datetime,
-        string_referencing=string_referencing,
-    ).encode(obj)
diff --git a/cbor2/tool.py b/cbor2/tool.py
index 44f938ba..e5631980 100644
--- a/cbor2/tool.py
+++ b/cbor2/tool.py
@@ -16,8 +16,7 @@
 from functools import partial
 from typing import TYPE_CHECKING, Any, TypeVar
 
-from . import CBORDecoder, CBORSimpleValue, CBORTag, load, undefined
-from .types import FrozenDict
+from . import CBORDecoder, CBORSimpleValue, CBORTag, FrozenDict, load, undefined
 
 if TYPE_CHECKING:
     from typing import Literal, TypeAlias
diff --git a/cbor2/types.py b/cbor2/types.py
index a39a7834..f4890891 100644
--- a/cbor2/types.py
+++ b/cbor2/types.py
@@ -1,211 +1,17 @@
-from __future__ import annotations
-
-from collections import namedtuple
-from collections.abc import Iterable, Iterator
-from functools import total_ordering
-from reprlib import recursive_repr
-from typing import Any, Mapping, TypeVar
-
-KT = TypeVar("KT")
-VT_co = TypeVar("VT_co", covariant=True)
-
-
-class CBORError(Exception):
-    """Base class for errors that occur during CBOR encoding or decoding."""
-
-
-class CBOREncodeError(CBORError):
-    """Raised for exceptions occurring during CBOR encoding."""
-
-
-class CBOREncodeTypeError(CBOREncodeError, TypeError):
-    """Raised when attempting to encode a type that cannot be serialized."""
-
-
-class CBOREncodeValueError(CBOREncodeError, ValueError):
-    """Raised when the CBOR encoder encounters an invalid value."""
-
-
-class CBORDecodeError(CBORError):
-    """Raised for exceptions occurring during CBOR decoding."""
-
-
-class CBORDecodeValueError(CBORDecodeError, ValueError):
-    """Raised when the CBOR stream being decoded contains an invalid value."""
-
-
-class CBORDecodeEOF(CBORDecodeError, EOFError):
-    """Raised when decoding unexpectedly reaches EOF."""
-
-
-@total_ordering
-class CBORTag:
-    """
-    Represents a CBOR semantic tag.
-
-    :param int tag: tag number
-    :param value: encapsulated value (any object)
-    """
-
-    __slots__ = "tag", "value"
-
-    def __init__(self, tag: str | int, value: Any) -> None:
-        if not isinstance(tag, int) or tag not in range(2**64):
-            raise TypeError("CBORTag tags must be positive integers less than 2**64")
-        self.tag = tag
-        self.value = value
-
-    def __eq__(self, other: object) -> bool:
-        if isinstance(other, CBORTag):
-            return (self.tag, self.value) == (other.tag, other.value)
-
-        return NotImplemented
-
-    def __le__(self, other: object) -> bool:
-        if isinstance(other, CBORTag):
-            return (self.tag, self.value) <= (other.tag, other.value)
-
-        return NotImplemented
-
-    @recursive_repr()
-    def __repr__(self) -> str:
-        return f"CBORTag({self.tag}, {self.value!r})"
-
-    def __hash__(self) -> int:
-        return hash((self.tag, self.value))
-
-
-class CBORSimpleValue(namedtuple("CBORSimpleValue", ["value"])):
-    """
-    Represents a CBOR "simple value".
-
-    :param int value: the value (0-255)
-    """
-
-    __slots__ = ()
-
-    value: int
-
-    def __hash__(self) -> int:
-        return hash(self.value)
-
-    def __new__(cls, value: int) -> CBORSimpleValue:
-        if value < 0 or value > 255:
-            raise TypeError("simple value out of range (0..255)")
-
-        return super().__new__(cls, value)
-
-    def __eq__(self, other: object) -> bool:
-        if isinstance(other, int):
-            return self.value == other
-        elif isinstance(other, CBORSimpleValue):
-            return self.value == other.value
-
-        return NotImplemented
-
-    def __ne__(self, other: object) -> bool:
-        if isinstance(other, int):
-            return self.value != other
-        elif isinstance(other, CBORSimpleValue):
-            return self.value != other.value
-
-        return NotImplemented
-
-    def __lt__(self, other: object) -> bool:
-        if isinstance(other, int):
-            return self.value < other
-        elif isinstance(other, CBORSimpleValue):
-            return self.value < other.value
-
-        return NotImplemented
-
-    def __le__(self, other: object) -> bool:
-        if isinstance(other, int):
-            return self.value <= other
-        elif isinstance(other, CBORSimpleValue):
-            return self.value <= other.value
-
-        return NotImplemented
-
-    def __ge__(self, other: object) -> bool:
-        if isinstance(other, int):
-            return self.value >= other
-        elif isinstance(other, CBORSimpleValue):
-            return self.value >= other.value
-
-        return NotImplemented
-
-    def __gt__(self, other: object) -> bool:
-        if isinstance(other, int):
-            return self.value > other
-        elif isinstance(other, CBORSimpleValue):
-            return self.value > other.value
-
-        return NotImplemented
-
-
-class FrozenDict(Mapping[KT, VT_co]):
-    """
-    A hashable, immutable mapping type.
-
-    The arguments to ``FrozenDict`` are processed just like those to ``dict``.
-    """
-
-    def __init__(self, *args: Mapping[KT, VT_co] | Iterable[tuple[KT, VT_co]]) -> None:
-        self._d: dict[KT, VT_co] = dict(*args)
-        self._hash: int | None = None
-
-    def __iter__(self) -> Iterator[KT]:
-        return iter(self._d)
-
-    def __len__(self) -> int:
-        return len(self._d)
-
-    def __getitem__(self, key: KT) -> VT_co:
-        return self._d[key]
-
-    def __repr__(self) -> str:
-        return f"{self.__class__.__name__}({self._d})"
-
-    def __hash__(self) -> int:
-        if self._hash is None:
-            self._hash = hash((frozenset(self), frozenset(self.values())))
-
-        return self._hash
-
-
-class UndefinedType:
-    __slots__ = ()
-
-    def __new__(cls: type[UndefinedType]) -> UndefinedType:
-        try:
-            return undefined
-        except NameError:
-            return super().__new__(cls)
-
-    def __repr__(self) -> str:
-        return "undefined"
-
-    def __bool__(self) -> bool:
-        return False
-
-
-class BreakMarkerType:
-    __slots__ = ()
-
-    def __new__(cls: type[BreakMarkerType]) -> BreakMarkerType:
-        try:
-            return break_marker
-        except NameError:
-            return super().__new__(cls)
-
-    def __repr__(self) -> str:
-        return "break_marker"
-
-    def __bool__(self) -> bool:
-        return True
-
-
-#: Represents the "undefined" value.
-undefined = UndefinedType()
-break_marker = BreakMarkerType()
+from warnings import warn
+
+from ._types import CBORDecodeEOF as CBORDecodeEOF
+from ._types import CBORDecodeError as CBORDecodeError
+from ._types import CBORDecodeValueError as CBORDecodeValueError
+from ._types import CBOREncodeError as CBOREncodeError
+from ._types import CBOREncodeTypeError as CBOREncodeTypeError
+from ._types import CBOREncodeValueError as CBOREncodeValueError
+from ._types import CBORError as CBORError
+from ._types import CBORSimpleValue as CBORSimpleValue
+from ._types import CBORTag as CBORTag
+from ._types import FrozenDict as FrozenDict
+from ._types import undefined as undefined
+
+warn(
+    "The cbor2.types module has been deprecated. Instead import everything directly from cbor2."
+)
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 00000000..5d12e85d
--- /dev/null
+++ b/docs/api.rst
@@ -0,0 +1,36 @@
+API reference
+=============
+
+Encoding
+--------
+
+.. autofunction:: cbor2.dumps
+.. autofunction:: cbor2.dump
+.. autoclass:: cbor2.CBOREncoder
+.. autodecorator:: cbor2.shareable_encoder
+
+Decoding
+--------
+
+.. autofunction:: cbor2.loads
+.. autofunction:: cbor2.load
+.. autoclass:: cbor2.CBORDecoder
+
+Types
+-----
+
+.. autoclass:: cbor2.CBORSimpleValue
+.. autoclass:: cbor2.CBORTag
+.. data:: cbor2.undefined
+    A singleton representing the CBOR "undefined" value.
+
+Exceptions
+----------
+
+.. autoexception:: cbor2.CBORError
+.. autoexception:: cbor2.CBOREncodeError
+.. autoexception:: cbor2.CBOREncodeTypeError
+.. autoexception:: cbor2.CBOREncodeValueError
+.. autoexception:: cbor2.CBORDecodeError
+.. autoexception:: cbor2.CBORDecodeValueError
+.. autoexception:: cbor2.CBORDecodeEOF
diff --git a/docs/conf.py b/docs/conf.py
index 34450f88..52b0cbb9 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -26,9 +26,8 @@
 pygments_style = "sphinx"
 highlight_language = "default"
 todo_include_todos = False
-autodoc_default_options = {
-    "special-members": "__init__",
-}
+autodoc_default_options = {"members": True}
+autodoc_mock_imports = ["typing_extensions"]
 
 html_theme = "sphinx_rtd_theme"
 htmlhelp_basename = project.replace("-", "") + "doc"
diff --git a/docs/customizing.rst b/docs/customizing.rst
index d1728378..02af2ff8 100644
--- a/docs/customizing.rst
+++ b/docs/customizing.rst
@@ -1,6 +1,8 @@
 Customizing encoding and decoding
 =================================
 
+.. py:currentmodule:: cbor2
+
 Both the encoder and decoder can be customized to support a wider range of types.
 
 On the encoder side, this is accomplished by passing a callback as the ``default`` constructor
@@ -18,7 +20,7 @@ only, it is recommended to use ``tag_hook`` for this purpose.
 Using the CBOR tags for custom types
 ------------------------------------
 
-The most common way to use ``default`` is to call :meth:`~cbor2.encoder.CBOREncoder.encode`
+The most common way to use ``default`` is to call :meth:`CBOREncoder.encode`
 to add a custom tag in the data stream, with the payload as the value::
 
     class Point:
@@ -79,7 +81,7 @@ make it possible to add special markers into the data stream which can be later
 substituted with the object marked earlier.
 
 To do this, in ``default`` hooks used with the encoder you will need to use the
-:meth:`~cbor2.encoder.shareable_encoder` decorator on your ``default`` hook function. It will
+:meth:`shareable_encoder` decorator on your ``default`` hook function. It will
 automatically automatically add the object to the shared values registry on the encoder and prevent
 it from being serialized twice (instead writing a reference to the data stream)::
 
@@ -92,7 +94,7 @@ it from being serialized twice (instead writing a reference to the data stream):
 
 On the decoder side, you will need to initialize an empty instance for shared value lookup before
 the object's state (which may contain references to it) is decoded.
-This is done with the :meth:`~cbor2.decoder.CBORDecoder.set_shareable` method::
+This is done with the :meth:`CBORDecoder.set_shareable` method::
 
     def tag_hook(decoder, tag, shareable_index=None):
         # Return all other tags as-is
diff --git a/docs/index.rst b/docs/index.rst
index 1d03f0a8..f813697f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,5 +1,8 @@
+CBOR2
+=====
+
 .. include:: ../README.rst
-   :start-line: 10
+   :start-line: 16
 
 Table of contents
 -----------------
@@ -9,9 +12,5 @@ Table of contents
 
    usage
    customizing
+   api
    versionhistory
-   Encoder <modules/encoder>
-   Decoder <modules/decoder>
-   Types <modules/types>
-
-* :ref:`API reference <modindex>`
diff --git a/docs/modules/decoder.rst b/docs/modules/decoder.rst
deleted file mode 100644
index c2c58fe9..00000000
--- a/docs/modules/decoder.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-:mod:`cbor2.decoder`
-====================
-
-.. automodule:: cbor2.decoder
-    :members:
diff --git a/docs/modules/encoder.rst b/docs/modules/encoder.rst
deleted file mode 100644
index c4240eea..00000000
--- a/docs/modules/encoder.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-:mod:`cbor2.encoder`
-====================
-
-.. automodule:: cbor2.encoder
-    :members:
diff --git a/docs/modules/types.rst b/docs/modules/types.rst
deleted file mode 100644
index a6dedaa3..00000000
--- a/docs/modules/types.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-:mod:`cbor2.types`
-==================
-
-.. automodule:: cbor2.types
-    :members:
diff --git a/docs/usage.rst b/docs/usage.rst
index 34a4c5d4..797db597 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -1,6 +1,8 @@
 Basic usage
 ===========
 
+.. py:currentmodule:: cbor2
+
 Serializing and deserializing with cbor2 is pretty straightforward::
 
     from cbor2 import dumps, loads, load
@@ -27,12 +29,12 @@ Date/time handling
 The CBOR specification does not support naïve datetimes (that is, datetimes where ``tzinfo`` is
 missing). When the encoder encounters such a datetime, it needs to know which timezone it belongs
 to. To this end, you can specify a default timezone by passing a :class:`~datetime.tzinfo` instance
-to :func:`~cbor2.encoder.dump`/:func:`~cbor2.encoder.dumps` call as the ``timezone`` argument.
+to :func:`dump`/:func:`dumps` call as the ``timezone`` argument.
 Decoded datetimes are always timezone aware.
 
 By default, datetimes are serialized in a manner that retains their timezone offsets. You can
 optimize the data stream size by passing ``datetime_as_timestamp=False`` to
-:func:`~cbor2.encoder.dump`/:func:`~cbor2.encoder.dumps`, but this causes the timezone offset
+:func:`dump`/:func:`dumps`, but this causes the timezone offset
 information to be lost.
 
 In versions prior to 4.2 the encoder would convert a ``datetime.date`` object into a
@@ -51,11 +53,11 @@ Cyclic (recursive) data structures
 ----------------------------------
 
 If the encoder encounters a shareable object (ie. list or dict) that it has seen before, it will
-by default raise :exc:`~cbor2.types.CBOREncodeError` indicating that a cyclic reference has been
+by default raise :exc:`CBOREncodeError` indicating that a cyclic reference has been
 detected and value sharing was not enabled. CBOR has, however, an extension specification that
 allows the encoder to reference a previously encoded value without processing it again. This makes
 it possible to serialize such cyclic references, but value sharing has to be enabled by passing
-``value_sharing=True`` to :func:`~cbor2.encoder.dump`/:func:`~cbor2.encoder.dumps`.
+``value_sharing=True`` to :func:`dump`/:func:`dumps`.
 
 .. warning:: Support for value sharing is rare in other CBOR implementations, so think carefully
     whether you want to enable it. It also causes some line overhead, as all potentially shareable
@@ -65,7 +67,7 @@ String references
 -----------------
 
 When ``string_referencing=True`` is passed to
-:func:`~cbor2.encoder.dump`/:func:`~cbor2.encoder.dumps`, if the encoder would encode a string that
+:func:`dump`/:func:`dumps`, if the encoder would encode a string that
 it has previously encoded and where a reference would be shorter than the encoded string, it
 instead encodes a reference to the nth sufficiently long string already encoded.
 
@@ -90,7 +92,7 @@ Tag   Semantics                                Python type(s)
 28    Mark shared value                        N/A
 29    Reference shared value                   N/A
 30    Rational number                          fractions.Fraction
-35    Regular expression                       ``_sre.SRE_Pattern`` (result of ``re.compile(...)``)
+35    Regular expression                       re.Pattern (result of ``re.compile(...)``)
 36    MIME message                             email.message.Message
 37    Binary UUID                              uuid.UUID
 256   String reference namespace               N/A
@@ -100,10 +102,10 @@ Tag   Semantics                                Python type(s)
 55799 Self-Described CBOR                      object
 ===== ======================================== ====================================================
 
-Arbitary tags can be represented with the :class:`~cbor2.types.CBORTag` class.
+Arbitary tags can be represented with the :class:`CBORTag` class.
 
 If you want to write a file that is detected as CBOR by the Unix ``file`` utility, wrap your data in
-a ``~cbor2.types.CBORTag`` object like so::
+a :class:`CBORTag` object like so::
 
     from cbor2 import dump, CBORTag
 
diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst
index 10f0f5ee..7f60e0e4 100644
--- a/docs/versionhistory.rst
+++ b/docs/versionhistory.rst
@@ -7,6 +7,9 @@ This library adheres to `Semantic Versioning <http://semver.org/>`_.
 
 **UNRELEASED**
 
+- The ``cbor2.encoder``, ``cbor2.decoder`` or ``cbor2.types`` modules were deprecated – import
+  their contents directly from ``cbor2`` from now on. The old modules will be removed in the next
+  major release.
 - Added support for Python 3.12
 - Added type annotations
 - Dropped support for Python 3.7
diff --git a/pyproject.toml b/pyproject.toml
index 800b7067..4a454f19 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -95,4 +95,9 @@ extras = test
 [testenv:docs]
 extras = doc
 commands = sphinx-build -W -n docs build/sphinx
+package = wheel
+
+[testenv:.pkg-docs]
+setenv =
+  CBOR2_BUILD_C_EXTENSION=0
 """
diff --git a/source/module.c b/source/module.c
index 49e2f2ba..8c13c2cd 100644
--- a/source/module.c
+++ b/source/module.c
@@ -402,7 +402,7 @@ _CBOR2_init_FrozenDict(void)
     PyObject *cbor2_types;
 
     // from cbor2.types import FrozenDict
-    cbor2_types = PyImport_ImportModule("cbor2.types");
+    cbor2_types = PyImport_ImportModule("cbor2._types");
     if (!cbor2_types)
         goto error;
     _CBOR2_FrozenDict = PyObject_GetAttr(cbor2_types, _CBOR2_str_FrozenDict);
@@ -412,7 +412,7 @@ _CBOR2_init_FrozenDict(void)
     return 0;
 error:
     PyErr_SetString(PyExc_ImportError,
-            "unable to import FrozenDict from cbor2.types");
+            "unable to import FrozenDict from cbor2._types");
     return -1;
 }
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 210476b9..2ef5db6c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,9 +3,9 @@
 
 import pytest
 
-import cbor2.decoder
-import cbor2.encoder
-import cbor2.types
+import cbor2._decoder
+import cbor2._encoder
+import cbor2._types
 
 load_exc = ""
 try:
@@ -46,7 +46,7 @@ def impl(request):
         # implementations, even if the top-level package has imported the
         # _cbor2 module
         module = Module()
-        for source in (cbor2.types, cbor2.encoder, cbor2.decoder):
+        for source in (cbor2._types, cbor2._encoder, cbor2._decoder):
             for name in dir(source):
                 setattr(module, name, getattr(source, name))
         return module
diff --git a/tests/hypothesis_strategies.py b/tests/hypothesis_strategies.py
index 6424103e..8c05418b 100644
--- a/tests/hypothesis_strategies.py
+++ b/tests/hypothesis_strategies.py
@@ -3,7 +3,7 @@
 
 from hypothesis import strategies
 
-from cbor2.types import FrozenDict
+from cbor2 import FrozenDict
 
 # Tune these for test run time
 MAX_SIZE = 5
diff --git a/tests/test_decoder.py b/tests/test_decoder.py
index 5f04d22a..5a7daa42 100644
--- a/tests/test_decoder.py
+++ b/tests/test_decoder.py
@@ -13,7 +13,7 @@
 
 import pytest
 
-from cbor2.types import FrozenDict
+from cbor2 import FrozenDict
 
 
 def test_fp_attr(impl):
diff --git a/tests/test_encoder.py b/tests/test_encoder.py
index f877a3ce..30d1f558 100644
--- a/tests/test_encoder.py
+++ b/tests/test_encoder.py
@@ -12,8 +12,7 @@
 import pytest
 from hypothesis import given
 
-from cbor2 import shareable_encoder
-from cbor2.types import FrozenDict
+from cbor2 import FrozenDict, shareable_encoder
 
 from .hypothesis_strategies import compound_types_strategy
 
diff --git a/tests/test_types.py b/tests/test_types.py
index e789c087..e3f18492 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -1,6 +1,6 @@
 import pytest
 
-from cbor2.types import FrozenDict
+from cbor2 import FrozenDict
 
 
 def test_undefined_bool(impl):