diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 1652e4bd..bf6601c4 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,6 @@ # How to Contribute -Thank you for taking the time to contribute to `msgspec`! +Thank you for taking the time to contribute to `msgspec-x`! Here we document some contribution guidelines to help you ensure that your contribution is at its best. @@ -17,17 +17,17 @@ Once you have those installed, you're ready to: - Clone the repository - Install all development dependencies -- Build a development version of `msgspec` +- Build a development version of `msgspec-x` - Install the `pre-commit` hooks ```bash # Clone the repository -git clone https://github.com/jcrist/msgspec.git +git clone https://github.com/nightsailer/msgspec-x.git # cd into the repo root directory -cd msgspec/ +cd msgspec-x/ -# Build and install msgspec & all dev dependencies +# Build and install msgspec-x & all dev dependencies pip install -e ".[dev]" # Install the pre-commit hooks @@ -36,9 +36,9 @@ pre-commit install ## Editing and Rebuilding -You now have a "development" build of `msgspec` installed. This means that you +You now have a "development" build of `msgspec-x` installed. This means that you can make changes to the `.py` files and test them without requiring a rebuild -of msgspec's C extension. Edit away! +of msgspec-x's C extension. Edit away! If you do make changes to a `.c` file, you'll need to recompile. You can do this by running @@ -47,7 +47,7 @@ this by running pip install -e . ``` -By default `msgspec` is built in release mode, with optimizations enabled. To +By default `msgspec-x` is built in release mode, with optimizations enabled. To build a debug build instead (for use with e.g. `gdb` or `lldb`) define the `MSGSPEC_DEBUG` environment variable before building. @@ -112,6 +112,6 @@ and fix any issues that come up. ## Code of Conduct -``msgspec`` has a code of conduct that must be followed by all contributors to +``msgspec-x`` has a code of conduct that must be followed by all contributors to the project. You may read the code of conduct -[here](https://github.com/jcrist/msgspec/blob/main/CODE_OF_CONDUCT.md). +[here](https://github.com/nightsailer/msgspec-x/blob/main/CODE_OF_CONDUCT.md). diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 69642142..0ba43770 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,5 +1,5 @@ name: 🪲 Bug Report -description: Report a bug or unexpected behavior in msgspec +description: Report a bug or unexpected behavior in msgspec-x body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 76170921..10e7c1f8 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -1,5 +1,5 @@ name: 🙌 Feature Request -description: Suggest a new feature or change to msgspec +description: Suggest a new feature or change to msgspec-x body: - type: markdown attributes: diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 2fd47a5b..9b33c631 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,7 +1,7 @@ # Security Policy -If you believe you have found a security-related bug with `msgspec`, **do not -open a public GitHub issue**. Instead, please email jcristharif@gmail.com. +If you believe you have found a security-related bug with `msgspec-x`, **do not +open a public GitHub issue**. Instead, please email nightsailer@gmail.com. Please include as much detail as you would for a normal issue in your report. In particular, including a minimal reproducible example will help the diff --git a/MANIFEST.in b/MANIFEST.in index 1332a61e..339d01d4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,9 @@ include msgspec/*.h include msgspec/*.py include msgspec/*.pyi include msgspec/py.typed +include msgspec_x/*.py +include msgspec_x/*.pyi +include msgspec_x/py.typed include setup.py include versioneer.py include README.md diff --git a/README.md b/README.md index ba26bc08..eeda43f4 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,141 @@ +# Msgspec-x + +

- + msgspec

- - + + - + - - + + - - + + - - + + - - + +

+## Overview -`msgspec` is a *fast* serialization and validation library, with builtin -support for [JSON](https://json.org), [MessagePack](https://msgpack.org), -[YAML](https://yaml.org), and [TOML](https://toml.io). It features: +`msgspec-x` is a community-driven fork of the [original msgspec library](https://jcristharif.com/msgspec/) by Jim Crist-Harif. This project was created to address the challenge of slow upstream maintenance and to provide a platform for community contributions that couldn't be timely integrated into the original project. -- 🚀 **High performance encoders/decoders** for common protocols. The JSON and - MessagePack implementations regularly - [benchmark](https://jcristharif.com/msgspec/benchmarks.html) as the fastest - options for Python. +### Why msgspec-x? -- 🎉 **Support for a wide variety of Python types**. Additional types may be - supported through - [extensions](https://jcristharif.com/msgspec/extending.html). +The original msgspec library is an excellent project, but the maintainer has limited time to review and merge community pull requests. This has resulted in valuable contributions and bug fixes being stuck in the review process. `msgspec-x` was created to: -- 🔍 **Zero-cost schema validation** using familiar Python type annotations. In - [benchmarks](https://jcristharif.com/msgspec/benchmarks.html) `msgspec` - decodes *and* validates JSON faster than - [orjson](https://github.com/ijl/orjson) can decode it alone. +- **Accelerate community contributions**: Provide a faster path for community PRs and enhancements +- **Enable rapid bug fixes**: Address issues without waiting for upstream review cycles +- **Extend functionality**: Add new features that complement the original design +- **Maintain compatibility**: Keep full backward compatibility with the original msgspec API -- ✨ **A speedy Struct type** for representing structured data. If you already - use [dataclasses](https://docs.python.org/3/library/dataclasses.html) or - [attrs](https://www.attrs.org), - [structs](https://jcristharif.com/msgspec/structs.html) should feel familiar. - However, they're - [5-60x faster](https://jcristharif.com/msgspec/benchmarks.html#benchmark-structs>) - for common operations. +### **⚠️ IMPORTANT: Installation Notice** -All of this is included in a -[lightweight library](https://jcristharif.com/msgspec/benchmarks.html#benchmark-library-size) -with no required dependencies. +**Do not install both `msgspec` and `msgspec-x` simultaneously!** They are conflicting packages that cannot coexist in the same environment. If you have the original `msgspec` installed, uninstall it first: + +```bash +pip uninstall msgspec +pip install msgspec-x +``` ---- +## Dual Namespace Architecture -`msgspec` may be used for serialization alone, as a faster JSON or -MessagePack library. For the greatest benefit though, we recommend using -`msgspec` to handle the full serialization & validation workflow: +`msgspec-x` provides two distinct namespaces to serve different needs: -**Define** your message schemas using standard Python type annotations. +### 1. `msgspec` Namespace - Full Compatibility +The `msgspec` namespace maintains 100% API compatibility with the original library. All your existing code will work without any changes: ```python ->>> import msgspec +import msgspec # Drop-in replacement for original msgspec ->>> class User(msgspec.Struct): -... """A new type describing a User""" -... name: str -... groups: set[str] = set() -... email: str | None = None +class User(msgspec.Struct): + name: str + email: str + +# All existing msgspec code works exactly the same +user = User("alice", "alice@example.com") +data = msgspec.json.encode(user) +decoded = msgspec.json.decode(data, type=User) ``` -**Encode** messages as JSON, or one of the many other supported protocols. +### 2. `msgspec_x` Namespace - Extended Features +The `msgspec_x` namespace provides additional functionality and enhancements not available in the original library: ```python ->>> alice = User("alice", groups={"admin", "engineering"}) +import msgspec_x # Extended features and community contributions + +# Extended features will be documented as they are added +# This namespace allows for innovative features without breaking compatibility +``` ->>> alice -User(name='alice', groups={"admin", "engineering"}, email=None) +## Core Features ->>> msg = msgspec.json.encode(alice) +`msgspec-x` inherits all the powerful features from the original msgspec library: ->>> msg -b'{"name":"alice","groups":["admin","engineering"],"email":null}' +- 🚀 **High performance encoders/decoders** for JSON, MessagePack, YAML, and TOML +- 🎉 **Support for a wide variety of Python types** with extension capabilities +- 🔍 **Zero-cost schema validation** using Python type annotations +- ✨ **Fast Struct type** for structured data representation +- 📦 **Lightweight library** with no required dependencies + +All protocols and performance characteristics are maintained from the original implementation. + +## Quick Start + +### Installation + +```bash +pip install msgspec-x ``` -**Decode** messages back into Python objects, with optional schema validation. +### Basic Usage + +Define your message schemas using standard Python type annotations: ```python ->>> msgspec.json.decode(msg, type=User) -User(name='alice', groups={"admin", "engineering"}, email=None) +import msgspec ->>> msgspec.json.decode(b'{"name":"bob","groups":[123]}', type=User) -Traceback (most recent call last): - File "", line 1, in -msgspec.ValidationError: Expected `str`, got `int` - at `$.groups[0]` +class User(msgspec.Struct): + """A new type describing a User""" + name: str + groups: set[str] = set() + email: str | None = None ``` -`msgspec` is designed to be as performant as possible, while retaining some of -the nicities of validation libraries like -[pydantic](https://pydantic-docs.helpmanual.io/). For supported types, -encoding/decoding a message with `msgspec` can be -[~10-80x faster than alternative libraries](https://jcristharif.com/msgspec/benchmarks.html). +Encode messages as JSON or other supported protocols: + +```python +alice = User("alice", groups={"admin", "engineering"}) +msg = msgspec.json.encode(alice) +# Output: b'{"name":"alice","groups":["admin","engineering"],"email":null}' +``` + +Decode messages back into Python objects with schema validation: + +```python +# Successful decoding +user = msgspec.json.decode(msg, type=User) + +# Validation error example +msgspec.json.decode(b'{"name":"bob","groups":[123]}', type=User) +# Raises: ValidationError: Expected `str`, got `int` - at `$.groups[0]` +``` + +## Performance + +`msgspec-x` maintains the same exceptional performance characteristics as the original msgspec library. In benchmarks, it can be 10-80x faster than alternative libraries for encoding/decoding with validation.

@@ -112,10 +143,25 @@ encoding/decoding a message with `msgspec` can be

-See [the documentation](https://jcristharif.com/msgspec/) for more information. +## Community & Contributing + +This project welcomes community contributions! Unlike the original project, we aim to provide faster review cycles and more responsive maintenance. + +- 🐛 **Bug Reports**: Issues are addressed promptly +- 🚀 **Feature Requests**: Community-driven feature development +- 🔧 **Pull Requests**: Faster review and merge process +- 📚 **Documentation**: Community-maintained documentation improvements + +## Documentation + +For detailed documentation, examples, and API references, visit: +- **Project Documentation**: [https://nightsailer.github.io/msgspec-x/](https://nightsailer.github.io/msgspec-x/) +- **Original msgspec docs**: [https://jcristharif.com/msgspec/](https://jcristharif.com/msgspec/) (for reference) + +## License +New BSD License. See the [License File](https://github.com/nightsailer/msgspec-x/blob/main/LICENSE). -## LICENSE +## Acknowledgments -New BSD. See the -[License File](https://github.com/jcrist/msgspec/blob/main/LICENSE). +Special thanks to Jim Crist-Harif for creating the original msgspec library. This fork exists to complement and extend his excellent work, not to replace it. diff --git a/docs/source/_templates/help.html b/docs/source/_templates/help.html index 919b6d3e..550a6e48 100644 --- a/docs/source/_templates/help.html +++ b/docs/source/_templates/help.html @@ -1,5 +1,5 @@

Need help?

- Open an issue in the issue tracker. -

+ Open an issue in the issue tracker. +

\ No newline at end of file diff --git a/docs/source/api.rst b/docs/source/api.rst index f7602e03..994cbc4e 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -8,6 +8,10 @@ Structs .. autoclass:: Struct +**StructMeta** + +The metaclass for Struct types. This class can be subclassed to create custom struct behaviors. See :ref:`struct-meta-subclasses` for detailed information on using StructMeta subclasses. + .. autofunction:: field .. autofunction:: defstruct diff --git a/docs/source/benchmarks.rst b/docs/source/benchmarks.rst index 39934a7e..7d3ec24a 100644 --- a/docs/source/benchmarks.rst +++ b/docs/source/benchmarks.rst @@ -2,6 +2,7 @@ Benchmarks ========== .. note:: + These benchmarks are for ``msgspec-x``, a community-driven fork of the original msgspec project. Benchmarks are *hard*. diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index b40eba3b..b52166bc 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -3,6 +3,90 @@ Changelog .. currentmodule:: msgspec +Version 0.20.0 (2025-06-21) +--------------------------- + +**🎉 MAJOR: Community Fork Release** + +This is the first release of ``msgspec-x``, a community-driven fork of the original msgspec library by Jim Crist-Harif. This fork was created to accelerate community contributions and provide a platform for extended features while maintaining full backward compatibility. + +**🚀 NEW MAJOR FEATURE: StructMeta Subclasses Support** + +- **Add comprehensive support for StructMeta subclasses** - the primary feature that motivated this fork. +- Enable custom metaclasses that inherit from `StructMeta` to work seamlessly with all msgspec functions. +- **TECHNICAL**: Modified C code to use `PyType_IsSubtype()` instead of direct type comparison for StructMeta detection. +- Affected functions now support StructMeta subclasses: + + - `msgspec.structs.asdict` - Convert struct instances to dictionaries + - `msgspec.structs.astuple` - Convert struct instances to tuples + - `msgspec.structs.replace` - Create modified copies of struct instances + - `msgspec.structs.force_setattr` - Force attribute setting on frozen structs + - JSON encoding/decoding operations + - MessagePack encoding/decoding operations + - `msgspec.convert` - Type conversion operations + - `msgspec.to_builtins` - Convert to builtin types + +- Comprehensive test coverage for StructMeta subclasses including: + + - Single-level StructMeta inheritance + - Multi-level StructMeta inheritance chains + - Integration with all struct utility functions + - Encoder/decoder compatibility testing + - Nested struct support with custom metaclasses + +- **Use Cases**: This enables advanced users to create custom struct behaviors through metaclass programming while maintaining full compatibility with msgspec's serialization ecosystem. + +**Project Rename and Fork** + +- **BREAKING**: Project renamed from ``msgspec`` to ``msgspec-x``. Do not install both packages simultaneously. +- Fork created due to slow upstream maintenance and to enable faster community contribution cycles. +- All project metadata, URLs, and documentation updated to reflect the new ``msgspec-x`` identity. +- Repository moved to ``https://github.com/nightsailer/msgspec-x``. +- Maintainer changed to Night Sailer (nightsailer@gmail.com). + +**Dual Namespace Architecture** + +- Introduce dual namespace architecture to support both compatibility and extensions: + + - ``msgspec`` namespace: 100% API compatibility with the original library for drop-in replacement. + - ``msgspec_x`` namespace: Extended features and community contributions (placeholder structure created). + +- All existing code using ``import msgspec`` will continue to work without changes. +- New extended features will be available under ``msgspec_x`` namespace. + +**Installation and Distribution** + +- Package name changed to ``msgspec-x`` on PyPI. +- Updated installation commands: ``pip install msgspec-x`` and ``conda install msgspec-x -c conda-forge``. +- Added clear warnings about not installing both ``msgspec`` and ``msgspec-x`` in the same environment. +- Updated versioneer configuration to use ``msgspec-x-`` prefix for source distributions. + +**Documentation Overhaul** + +- Comprehensive documentation update to reflect the project fork and new architecture. +- Added explanation of the dual namespace system and community-driven development model. +- Updated all GitHub links, issue tracker URLs, and example source references. +- Enhanced installation documentation with compatibility warnings. +- Updated contributing guidelines and security policies for the new project structure. + +**Community and Development** + +- Established faster review and merge cycles for community contributions. +- Updated GitHub issue templates and workflows for the new repository. +- Created placeholder structure for experimental features in ``msgspec_x`` namespace. +- Enhanced project documentation to welcome community contributions. + +**Technical Infrastructure** + +- Updated build configuration (``setup.py``, ``setup.cfg``, ``MANIFEST.in``) for the new package structure. +- Enhanced CI/CD workflows for the dual namespace architecture. +- Updated type stub files and package metadata for both namespaces. +- Maintained all existing performance characteristics and API compatibility. + +**Acknowledgments** + +This release acknowledges and thanks Jim Crist-Harif for creating the original msgspec library. This fork exists to complement and extend his excellent work, not to replace it. + Version 0.19.0 (2024-12-27) --------------------------- diff --git a/docs/source/conf.py b/docs/source/conf.py index 668f3a79..a8d4469e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,9 +18,9 @@ class UnsetType: pass -project = "msgspec" -copyright = "Jim Crist-Harif" -author = "Jim Crist-Harif" +project = "msgspec-x" +copyright = "Night Sailer" +author = "Night Sailer" GITHUB_LOGO = """ @@ -70,7 +70,7 @@ class UnsetType: "footer_icons": [ { "name": "GitHub", - "url": "https://github.com/jcrist/msgspec", + "url": "https://github.com/nightsailer/msgspec-x", "html": GITHUB_LOGO, "class": "", }, @@ -97,8 +97,8 @@ class UnsetType: napoleon_custom_sections = [("Configuration", "params_style")] default_role = "obj" extlinks = { - "issue": ("https://github.com/jcrist/msgspec/issues/%s", "Issue #%s"), - "pr": ("https://github.com/jcrist/msgspec/pull/%s", "PR #%s"), + "issue": ("https://github.com/nightsailer/msgspec-x/issues/%s", "Issue #%s"), + "pr": ("https://github.com/nightsailer/msgspec-x/pull/%s", "PR #%s"), } copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: " copybutton_prompt_is_regexp = True diff --git a/docs/source/converters.rst b/docs/source/converters.rst index cbdabd99..8955fc49 100644 --- a/docs/source/converters.rst +++ b/docs/source/converters.rst @@ -127,9 +127,9 @@ support by wrapping the standard library's `tomllib` module as follows: ``msgspec`` uses these APIs to implement ``toml`` and ``yaml`` support, wrapping external serialization libraries: -- ``msgspec.toml`` (`code `__) +- ``msgspec.toml`` (`code `__) -- ``msgspec.yaml`` (`code `__) +- ``msgspec.yaml`` (`code `__) The implementation in ``msgspec.toml`` is *almost* identical to the one above, with some additional code for error handling. diff --git a/docs/source/examples/geojson.rst b/docs/source/examples/geojson.rst index 7557f167..bb5298c9 100644 --- a/docs/source/examples/geojson.rst +++ b/docs/source/examples/geojson.rst @@ -21,7 +21,7 @@ types, and :ref:`struct-tagged-unions` to differentiate between them. See the relevant docs for more information. The full example source can be found `here -`__. +`__. .. literalinclude:: ../../../examples/geojson/msgspec_geojson.py :language: python diff --git a/docs/source/examples/pyproject-toml.rst b/docs/source/examples/pyproject-toml.rst index 1d171c2f..9f02edc7 100644 --- a/docs/source/examples/pyproject-toml.rst +++ b/docs/source/examples/pyproject-toml.rst @@ -23,7 +23,7 @@ file. This includes full schema definitions for all fields in the ``tool``. The full example source can be found `here -`__. +`__. .. literalinclude:: ../../../examples/pyproject-toml/pyproject.py :language: python diff --git a/docs/source/index.rst b/docs/source/index.rst index f230af1f..71265ec4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,7 +1,13 @@ -msgspec -======= +msgspec-x +========= -``msgspec`` is a *fast* serialization and validation library, with builtin +``msgspec-x`` is a community-driven fork of the original msgspec library. It provides two namespaces: +- ``msgspec``: 100% compatible with the original API for drop-in replacement. +- ``msgspec_x``: Extended features and community contributions. + +Do not install both ``msgspec`` and ``msgspec-x`` in the same environment. + +``msgspec-x`` is a *fast* serialization and validation library, with builtin support for JSON_, MessagePack_, YAML_, and TOML_. It features: - 🚀 **High performance encoders/decoders** for common protocols. The JSON and @@ -12,21 +18,25 @@ support for JSON_, MessagePack_, YAML_, and TOML_. It features: be supported through :doc:`extensions `. - 🔍 **Zero-cost schema validation** using familiar Python type annotations. - In :doc:`benchmarks ` ``msgspec`` decodes *and* validates JSON + In :doc:`benchmarks ` ``msgspec-x`` decodes *and* validates JSON faster than orjson_ can decode it alone. - ✨ **A speedy Struct type** for representing structured data. If you already use dataclasses_ or attrs_, :doc:`structs` should feel familiar. However, they're :ref:`5-60x ` faster for common operations. +- 🆕 **StructMeta subclasses support** for advanced metaclass programming. + Create custom struct behaviors while maintaining full compatibility with + all msgspec operations. See :ref:`struct-meta-subclasses` for details. + All of this is included in a :ref:`lightweight library ` with no required dependencies. ----- -``msgspec`` may be used for serialization alone, as a faster JSON or +``msgspec-x`` may be used for serialization alone, as a faster JSON or MessagePack library. For the greatest benefit though, we recommend using -``msgspec`` to handle the full serialization & validation workflow: +``msgspec-x`` to handle the full serialization & validation workflow: **Define** your message schemas using standard Python type annotations. @@ -66,40 +76,45 @@ MessagePack library. For the greatest benefit though, we recommend using File "", line 1, in msgspec.ValidationError: Expected `str`, got `int` - at `$.groups[0]` -``msgspec`` is designed to be as performant as possible, while retaining some +``msgspec-x`` is designed to be as performant as possible, while retaining some of the nicities of validation libraries like pydantic_. For supported types, -encoding/decoding a message with ``msgspec`` can be :doc:`~10-80x faster than +encoding/decoding a message with ``msgspec-x`` can be :doc:`~10-80x faster than alternative libraries `. Highlights ---------- -- ``msgspec`` is **fast**. It :doc:`benchmarks ` as the fastest +- ``msgspec-x`` is **fast**. It :doc:`benchmarks ` as the fastest serialization library for Python, outperforming all other JSON/MessagePack libraries compared. -- ``msgspec`` is **friendly**. Through use of Python's type annotations, +- ``msgspec-x`` is **friendly**. Through use of Python's type annotations, messages are :ref:`validated ` during deserialization in a - declarative way. ``msgspec`` also works well with other type-checking tooling + declarative way. ``msgspec-x`` also works well with other type-checking tooling like mypy_ and pyright_, providing excellent editor integration. -- ``msgspec`` is **flexible**. It natively supports a :doc:`wide range of +- ``msgspec-x`` is **flexible**. It natively supports a :doc:`wide range of Python builtin types `. Support for additional types can also be added through :doc:`extensions `. -- ``msgspec`` is **lightweight**. It has no required dependencies, and the +- ``msgspec-x`` is **lightweight**. It has no required dependencies, and the binary size is :ref:`a fraction of that of comparable libraries `. -- ``msgspec`` is **correct**. The encoders/decoders implemented are strictly +- ``msgspec-x`` is **correct**. The encoders/decoders implemented are strictly compliant with their respective specifications, providing stronger guarantees of compatibility with other systems. +- ``msgspec-x`` is **extensible**. The new :ref:`StructMeta subclasses + ` support enables advanced users to create custom + struct behaviors through metaclass programming while maintaining full + compatibility with all msgspec operations. + Used By ------- -``msgspec`` is used by many organizations and `open source projects -`__, here we highlight a +``msgspec-x`` is used by many organizations and `open source projects +`__, here we highlight a few: .. grid:: 2 2 4 4 @@ -195,4 +210,4 @@ few: api.rst examples/index.rst - changelog.rst + changelog.rst \ No newline at end of file diff --git a/docs/source/inspect.rst b/docs/source/inspect.rst index 4c89b0b4..0bdc8bf9 100644 --- a/docs/source/inspect.rst +++ b/docs/source/inspect.rst @@ -142,7 +142,7 @@ subclass. See the :ref:`API docs ` for a complete list of types. For an example of using these functions, you might find our builtin :doc:`jsonschema` generator implementation useful - the code for this can be found `here -`__. In +`__. In particular, take a look at the large if-else statement in ``_to_schema``. diff --git a/docs/source/install.rst b/docs/source/install.rst index d292abbd..1b8c96a4 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -1,26 +1,28 @@ Installation ============ -``msgspec`` may be installed via ``pip`` or ``conda``. Note that Python >= 3.8 -is required. The basic install has no required dependencies. +``msgspec-x`` may be installed via ``pip`` or ``conda``. Note that Python >= 3.9 is required. The basic install has no required dependencies. **pip** .. code-block:: shell - pip install msgspec + pip install msgspec-x **conda** .. code-block:: shell - conda install msgspec -c conda-forge + conda install msgspec-x -c conda-forge + +.. note:: + Do not install both ``msgspec`` and ``msgspec-x`` in the same environment. Optional Dependencies --------------------- -Depending on your platform, the base install of ``msgspec`` may not support +Depending on your platform, the base install of ``msgspec-x`` may not support TOML_ or YAML_ without additional dependencies. TOML @@ -40,13 +42,13 @@ extra: .. code-block:: shell - pip install "msgspec[toml]" + pip install "msgspec-x[toml]" **conda** .. code-block:: shell - conda install msgspec-toml -c conda-forge + conda install msgspec-x-toml -c conda-forge YAML ~~~~ @@ -58,13 +60,13 @@ this dependency manually, or depend on the ``yaml`` extra: .. code-block:: shell - pip install "msgspec[yaml]" + pip install "msgspec-x[yaml]" **conda** .. code-block:: shell - conda install msgspec-yaml -c conda-forge + conda install msgspec-x-yaml -c conda-forge Installing from GitHub diff --git a/docs/source/structs.rst b/docs/source/structs.rst index 03667d0c..95a8d2d0 100644 --- a/docs/source/structs.rst +++ b/docs/source/structs.rst @@ -998,6 +998,201 @@ container types. It is your responsibility to ensure cycles with these objects don't occur, as a cycle containing only ``gc=False`` structs will *never* be collected (leading to a memory leak). + +.. _struct-meta-subclasses: + +StructMeta Subclasses (Advanced) +--------------------------------- + +.. versionadded:: 0.20.0 + +``msgspec-x`` provides comprehensive support for custom metaclasses that inherit from `msgspec.StructMeta`. This advanced feature enables users to create custom struct behaviors through metaclass programming while maintaining full compatibility with msgspec's serialization ecosystem. + +**What are StructMeta Subclasses?** + +StructMeta subclasses allow you to extend or customize the behavior of struct creation and management by defining your own metaclass that inherits from `msgspec.StructMeta`. This enables advanced patterns like: + +- Adding custom validation logic during struct class creation +- Implementing custom field processing or transformation +- Integrating with external frameworks or ORMs +- Creating domain-specific struct variants with specialized behaviors + +**Basic Usage** + +Here's a simple example of creating and using a custom StructMeta subclass: + +.. code-block:: python + + >>> import msgspec + >>> from msgspec import StructMeta + + >>> class CustomMeta(StructMeta): + ... """A custom metaclass that extends StructMeta""" + ... def __new__(cls, name, bases, namespace): + ... # Custom logic during class creation + ... print(f"Creating struct class: {name}") + ... return super().__new__(cls, name, bases, namespace) + + >>> class CustomStruct(metaclass=CustomMeta): + ... x: int + ... y: str + ... z: float = 3.14 + Creating struct class: CustomStruct + + >>> # Instances work exactly like regular structs + ... obj = CustomStruct(x=42, y="hello") + >>> obj + CustomStruct(x=42, y='hello', z=3.14) + +**Full Compatibility with msgspec Functions** + +Structures created with StructMeta subclasses work seamlessly with all msgspec operations: + +.. code-block:: python + + >>> from msgspec.structs import asdict, astuple, replace, force_setattr + + >>> # All struct utility functions work + >>> asdict(obj) + {'x': 42, 'y': 'hello', 'z': 3.14} + + >>> astuple(obj) + (42, 'hello', 3.14) + + >>> replace(obj, x=100) + CustomStruct(x=100, y='hello', z=3.14) + + >>> # JSON encoding/decoding works + >>> import msgspec.json + >>> data = msgspec.json.encode(obj) + >>> msgspec.json.decode(data, type=CustomStruct) + CustomStruct(x=42, y='hello', z=3.14) + + >>> # MessagePack encoding/decoding works + >>> import msgspec.msgpack + >>> data = msgspec.msgpack.encode(obj) + >>> msgspec.msgpack.decode(data, type=CustomStruct) + CustomStruct(x=42, y='hello', z=3.14) + + >>> # Type conversion works + >>> msgspec.convert({'x': 1, 'y': 'test', 'z': 2.5}, type=CustomStruct) + CustomStruct(x=1, y='test', z=2.5) + +**Multi-Level Inheritance** + +StructMeta subclasses support inheritance chains, allowing for sophisticated metaclass hierarchies: + +.. code-block:: python + + >>> class BaseMeta(StructMeta): + ... """Base custom metaclass""" + ... pass + + >>> class DerivedMeta(BaseMeta): + ... """Derived custom metaclass""" + ... def __new__(cls, name, bases, namespace): + ... # Add custom behavior + ... result = super().__new__(cls, name, bases, namespace) + ... result._custom_attribute = f"Enhanced {name}" + ... return result + + >>> class EnhancedStruct(metaclass=DerivedMeta): + ... value: int + ... name: str + + >>> obj = EnhancedStruct(value=123, name="test") + >>> obj._custom_attribute + 'Enhanced EnhancedStruct' + + >>> # All msgspec functions still work + >>> asdict(obj) + {'value': 123, 'name': 'test'} + +**Nested Structures** + +StructMeta subclasses work correctly with nested structures and complex serialization scenarios: + +.. code-block:: python + + >>> class ContainerMeta(StructMeta): + ... """Metaclass for container structures""" + ... pass + + >>> class Item(metaclass=ContainerMeta): + ... id: int + ... name: str + + >>> class Container(metaclass=ContainerMeta): + ... items: list[Item] + ... count: int + + >>> container = Container( + ... items=[Item(id=1, name="first"), Item(id=2, name="second")], + ... count=2 + ... ) + + >>> # Complex nested encoding/decoding works + >>> data = msgspec.json.encode(container) + >>> decoded = msgspec.json.decode(data, type=Container) + >>> decoded.items[0].name + 'first' + +**Integration with Struct Options** + +StructMeta subclasses work with all struct configuration options: + +.. code-block:: python + + >>> class FrozenMeta(StructMeta): + ... """Metaclass for immutable structures""" + ... pass + + >>> class ImmutablePoint(metaclass=FrozenMeta, frozen=True, order=True): + ... x: float + ... y: float + + >>> p1 = ImmutablePoint(1.0, 2.0) + >>> p2 = ImmutablePoint(3.0, 4.0) + + >>> # Frozen behavior works + >>> try: + ... p1.x = 5.0 + ... except AttributeError as e: + ... print(f"Expected error: {e}") + Expected error: immutable type: 'ImmutablePoint' + + >>> # Ordering works + >>> p1 < p2 + True + + >>> # All msgspec functions work + >>> replace(p1, x=10.0) + ImmutablePoint(x=10.0, y=2.0) + +**Technical Implementation** + +``msgspec-x`` achieves StructMeta subclass support by modifying the core C implementation to use `PyType_IsSubtype()` checks instead of direct type comparisons. This change affects all core msgspec operations including: + +- Type validation during encoding/decoding +- Struct utility functions (asdict, astuple, replace, etc.) +- Type conversion operations +- Tagged union resolution +- Performance optimizations + +The implementation maintains full backward compatibility - existing code continues to work unchanged, while new code can take advantage of the enhanced metaclass support. + +**Use Cases** + +StructMeta subclasses enable advanced patterns such as: + +- **Framework Integration**: Creating structs that automatically integrate with web frameworks, ORMs, or validation libraries +- **Domain-Specific Languages**: Building specialized struct types for specific problem domains +- **Automatic Documentation**: Metaclasses that generate documentation or schema information +- **Validation Enhancement**: Adding complex validation logic at the class level +- **Serialization Customization**: Implementing custom serialization behaviors for specific use cases + +This feature is particularly valuable for library authors who want to build higher-level abstractions on top of msgspec's fast serialization capabilities. + .. _type annotations: https://docs.python.org/3/library/typing.html .. _pattern matching: https://docs.python.org/3/reference/compound_stmts.html#the-match-statement .. _PEP 636: https://peps.python.org/pep-0636/ diff --git a/docs/source/supported-types.rst b/docs/source/supported-types.rst index 082e667a..ce4b976d 100644 --- a/docs/source/supported-types.rst +++ b/docs/source/supported-types.rst @@ -1616,5 +1616,9 @@ TOML_ types are decoded to Python types as follows: .. _pyright: https://github.com/microsoft/pyright .. _generic types: .. _user-defined generic types: https://docs.python.org/3/library/typing.html#user-defined-generic-types -.. _open an issue: https://github.com/jcrist/msgspec/issues> +.. _open an issue: https://github.com/nightsailer/msgspec-x/issues> .. _ISO 8601 duration strings: https://en.wikipedia.org/wiki/ISO_8601#Durations + +- ``msgspec`` is used by many organizations and `open source projects + `__, here we highlight a + few: diff --git a/msgspec/__init__.py b/msgspec/__init__.py index 421e4311..95079e7d 100644 --- a/msgspec/__init__.py +++ b/msgspec/__init__.py @@ -6,6 +6,7 @@ MsgspecError, Raw, Struct, + StructMeta, UnsetType, UNSET, NODEFAULT, diff --git a/msgspec/__init__.pyi b/msgspec/__init__.pyi index 4d2d8744..811426a3 100644 --- a/msgspec/__init__.pyi +++ b/msgspec/__init__.pyi @@ -1,4 +1,5 @@ import enum +from inspect import Signature from typing import ( Any, Callable, @@ -20,6 +21,43 @@ from typing_extensions import dataclass_transform, Buffer from . import inspect, json, msgpack, structs, toml, yaml +class StructMeta(type): + __struct_fields__: ClassVar[Tuple[str, ...]] + __struct_defaults__: ClassVar[Tuple[Any, ...]] + __struct_encode_fields__: ClassVar[Tuple[str, ...]] + __match_args__: ClassVar[Tuple[str, ...]] + @property + def __signature__(self) -> Signature: ... + @property + def __struct_config__(self) -> structs.StructConfig: ... + def __new__( + cls, + name: str, + bases: Tuple[type, ...], + namespace: Dict[str, Any], + *, + tag_field: Optional[str] = None, + tag: Union[None, bool, str, int, Callable[[str], Union[str, int]]] = None, + rename: Union[ + None, + Literal["lower", "upper", "camel", "pascal", "kebab"], + Callable[[str], Optional[str]], + Mapping[str, str], + ] = None, + omit_defaults: bool = False, + forbid_unknown_fields: bool = False, + frozen: bool = False, + eq: bool = True, + order: bool = False, + kw_only: bool = False, + repr_omit_defaults: bool = False, + array_like: bool = False, + gc: bool = True, + weakref: bool = False, + dict: bool = False, + cache_hash: bool = False, + ) -> StructMeta: ... + T = TypeVar("T") class UnsetType(enum.Enum): @@ -39,7 +77,7 @@ def field(*, default_factory: Callable[[], T], name: Optional[str] = None) -> T: @overload def field(*, name: Optional[str] = None) -> Any: ... @dataclass_transform(field_specifiers=(field,)) -class Struct: +class Struct(metaclass=StructMeta): __struct_fields__: ClassVar[Tuple[str, ...]] __struct_config__: ClassVar[structs.StructConfig] __match_args__: ClassVar[Tuple[str, ...]] diff --git a/msgspec/_core.c b/msgspec/_core.c index 147ad95a..26659660 100644 --- a/msgspec/_core.c +++ b/msgspec/_core.c @@ -4862,8 +4862,8 @@ typenode_collect_type(TypeNodeCollectState *state, PyObject *obj) { out = typenode_collect_typevar(state, t); } else if ( - Py_TYPE(t) == &StructMetaType || - (origin != NULL && Py_TYPE(origin) == &StructMetaType) + PyType_IsSubtype(Py_TYPE(t), &StructMetaType) || + (origin != NULL && PyType_IsSubtype(Py_TYPE(origin), &StructMetaType)) ) { out = typenode_collect_struct(state, t); } @@ -5517,7 +5517,7 @@ structmeta_collect_base(StructMetaInfo *info, MsgspecState *mod, PyObject *base) return -1; } - if (Py_TYPE(base) != &StructMetaType) { + if (!PyType_IsSubtype(Py_TYPE(base), &StructMetaType)) { if (((PyTypeObject *)base)->tp_dictoffset) { info->has_non_slots_bases = true; } @@ -5724,7 +5724,7 @@ structmeta_process_default(StructMetaInfo *info, PyObject *name) { if (default_val == NULL) return -1; } else if ( - (Py_TYPE(type) == &StructMetaType) && + (PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) && ((StructMetaObject *)type)->frozen != OPT_TRUE ) { goto error_mutable_struct; @@ -6685,7 +6685,7 @@ StructInfo_Convert(PyObject *obj) { PyObject *annotations = NULL; StructInfo *info = NULL; bool cache_set = false; - bool is_struct = Py_TYPE(obj) == &StructMetaType; + bool is_struct = PyType_IsSubtype(Py_TYPE(obj), &StructMetaType); /* Check for a cached StructInfo, and return if one exists */ if (MS_LIKELY(is_struct)) { @@ -6703,7 +6703,7 @@ StructInfo_Convert(PyObject *obj) { } PyObject *origin = PyObject_GetAttr(obj, mod->str___origin__); if (origin == NULL) return NULL; - if (Py_TYPE(origin) != &StructMetaType) { + if (!PyType_IsSubtype(Py_TYPE(origin), &StructMetaType)) { Py_DECREF(origin); PyErr_SetString( PyExc_RuntimeError, "Expected __origin__ to be a Struct type" @@ -7146,7 +7146,7 @@ static PyTypeObject StructMetaType = { .tp_name = "msgspec._core.StructMeta", .tp_basicsize = sizeof(StructMetaObject), .tp_itemsize = 0, - .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_TYPE_SUBCLASS | Py_TPFLAGS_HAVE_GC | _Py_TPFLAGS_HAVE_VECTORCALL, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_TYPE_SUBCLASS | Py_TPFLAGS_HAVE_GC | _Py_TPFLAGS_HAVE_VECTORCALL | Py_TPFLAGS_BASETYPE, .tp_new = StructMeta_new, .tp_dealloc = (destructor) StructMeta_dealloc, .tp_clear = (inquiry) StructMeta_clear, @@ -7785,7 +7785,7 @@ struct_replace(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject if (!check_positional_nargs(nargs, 1, 1)) return NULL; PyObject *obj = args[0]; - if (Py_TYPE(Py_TYPE(obj)) != &StructMetaType) { + if (!PyType_IsSubtype(Py_TYPE(Py_TYPE(obj)), &StructMetaType)) { PyErr_SetString(PyExc_TypeError, "`struct` must be a `msgspec.Struct`"); return NULL; } @@ -7826,7 +7826,7 @@ struct_asdict(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { if (!check_positional_nargs(nargs, 1, 1)) return NULL; PyObject *obj = args[0]; - if (Py_TYPE(Py_TYPE(obj)) != &StructMetaType) { + if (!PyType_IsSubtype(Py_TYPE(Py_TYPE(obj)), &StructMetaType)) { PyErr_SetString(PyExc_TypeError, "`struct` must be a `msgspec.Struct`"); return NULL; } @@ -7884,7 +7884,7 @@ struct_astuple(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { if (!check_positional_nargs(nargs, 1, 1)) return NULL; PyObject *obj = args[0]; - if (Py_TYPE(Py_TYPE(obj)) != &StructMetaType) { + if (!PyType_IsSubtype(Py_TYPE(Py_TYPE(obj)), &StructMetaType)) { PyErr_SetString(PyExc_TypeError, "`struct` must be a `msgspec.Struct`"); return NULL; } @@ -7936,7 +7936,7 @@ struct_force_setattr(PyObject *self, PyObject *const *args, Py_ssize_t nargs) PyObject *obj = args[0]; PyObject *name = args[1]; PyObject *value = args[2]; - if (Py_TYPE(Py_TYPE(obj)) != &StructMetaType) { + if (!PyType_IsSubtype(Py_TYPE(Py_TYPE(obj)), &StructMetaType)) { PyErr_SetString(PyExc_TypeError, "`struct` must be a `msgspec.Struct`"); return NULL; } @@ -13068,7 +13068,7 @@ mpack_encode_uncommon(EncoderState *self, PyTypeObject *type, PyObject *obj) else if (type == &PyBool_Type) { return mpack_encode_bool(self, obj); } - else if (Py_TYPE(type) == &StructMetaType) { + else if (PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) { return mpack_encode_struct(self, obj); } else if (type == &PyBytes_Type) { @@ -14148,7 +14148,7 @@ json_encode_uncommon(EncoderState *self, PyTypeObject *type, PyObject *obj) { else if (obj == Py_False) { return ms_write(self, "false", 5); } - else if (Py_TYPE(type) == &StructMetaType) { + else if (PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) { return json_encode_struct(self, obj); } else if (PyTuple_Check(obj)) { @@ -16273,7 +16273,7 @@ msgspec_msgpack_decode(PyObject *self, PyObject *const *args, Py_ssize_t nargs, if (type == NULL || type == mod->typing_any) { state.type = &typenode_any; } - else if (Py_TYPE(type) == &StructMetaType) { + else if (PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) { PyObject *info = StructInfo_Convert(type); if (info == NULL) return NULL; bool array_like = ((StructMetaObject *)type)->array_like == OPT_TRUE; @@ -19304,7 +19304,7 @@ msgspec_json_decode(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyO if (type == NULL || type == mod->typing_any) { state.type = &typenode_any; } - else if (Py_TYPE(type) == &StructMetaType) { + else if (PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) { PyObject *info = StructInfo_Convert(type); if (info == NULL) return NULL; bool array_like = ((StructMetaObject *)type)->array_like == OPT_TRUE; @@ -19855,7 +19855,7 @@ to_builtins(ToBuiltinsState *self, PyObject *obj, bool is_key) { else if (PyDict_Check(obj)) { return to_builtins_dict(self, obj); } - else if (Py_TYPE(type) == &StructMetaType) { + else if (PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) { return to_builtins_struct(self, obj, is_key); } else if (Py_TYPE(type) == self->mod->EnumMetaType) { @@ -21749,7 +21749,7 @@ msgspec_convert(PyObject *self, PyObject *args, PyObject *kwargs) state.dec_hook = dec_hook; /* Avoid allocating a new TypeNode for struct types */ - if (Py_TYPE(pytype) == &StructMetaType) { + if (PyType_IsSubtype(Py_TYPE(pytype), &StructMetaType)) { PyObject *info = StructInfo_Convert(pytype); if (info == NULL) return NULL; bool array_like = ((StructMetaObject *)pytype)->array_like == OPT_TRUE; @@ -22069,7 +22069,13 @@ PyInit__core(void) return NULL; Py_INCREF(&Unset_Type); if (PyModule_AddObject(m, "UnsetType", (PyObject *)&Unset_Type) < 0) + return NULL; + Py_INCREF((PyObject *)&StructMetaType); + if (PyModule_AddObject(m, "StructMeta", (PyObject *)&StructMetaType) < 0) { + Py_DECREF((PyObject *)&StructMetaType); + Py_DECREF(m); return NULL; + } st = msgspec_get_state(m); diff --git a/msgspec_x/__init__.py b/msgspec_x/__init__.py new file mode 100644 index 00000000..b41f4c36 --- /dev/null +++ b/msgspec_x/__init__.py @@ -0,0 +1,10 @@ +""" +msgspec_x - Extended features for msgspec + +This namespace provides additional functionality and community contributions +that extend the original msgspec library capabilities. + + +For compatibility with existing code, use the `msgspec` namespace. +For new extended features, use this `msgspec_x` namespace. +""" diff --git a/setup.cfg b/setup.cfg index 4e937c4d..84f37976 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,4 +21,4 @@ style = pep440 versionfile_source = msgspec/_version.py versionfile_build = msgspec/_version.py tag_prefix = -parentdir_prefix = msgspec- +parentdir_prefix = msgspec-x- diff --git a/setup.py b/setup.py index 2a92252f..dcecf930 100644 --- a/setup.py +++ b/setup.py @@ -14,10 +14,10 @@ error = """ ==================================================================== - `msgspec` currently doesn't support 32-bit Python windows builds. If + `msgspec-x` currently doesn't support 32-bit Python windows builds. If this is important for your use case, please open an issue on GitHub: - https://github.com/jcrist/msgspec/issues + https://github.com/nightsailer/msgspec-x/issues ==================================================================== """ print(textwrap.dedent(error)) @@ -70,22 +70,23 @@ } setup( - name="msgspec", + name="msgspec-x", version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), - maintainer="Jim Crist-Harif", - maintainer_email="jcristharif@gmail.com", - url="https://jcristharif.com/msgspec/", + maintainer="Night Sailer", + maintainer_email="nightsailer@gmail.com", + url="https://nightsailer.github.io/msgspec-x/", project_urls={ - "Documentation": "https://jcristharif.com/msgspec/", - "Source": "https://github.com/jcrist/msgspec/", - "Issue Tracker": "https://github.com/jcrist/msgspec/issues", + "Documentation": "https://nightsailer.github.io/msgspec-x/", + "Source": "https://github.com/nightsailer/msgspec-x/", + "Issue Tracker": "https://github.com/nightsailer/msgspec-x/issues", }, description=( - "A fast serialization and validation library, with builtin support for " - "JSON, MessagePack, YAML, and TOML." + "A community-driven fork of msgspec: fast serialization and validation library with " + "builtin support for JSON, MessagePack, YAML, and TOML. Provides dual namespace " + "architecture - 'msgspec' for full compatibility and 'msgspec_x' for extended features." ), - keywords="JSON msgpack MessagePack TOML YAML serialization validation schema", + keywords="JSON msgpack MessagePack TOML YAML serialization validation schema fork community", classifiers=[ "License :: OSI Approved :: BSD License", "Development Status :: 4 - Beta", @@ -97,8 +98,8 @@ ], extras_require=extras_require, license="BSD", - packages=["msgspec"], - package_data={"msgspec": ["py.typed", "*.pyi"]}, + packages=["msgspec", "msgspec_x"], + package_data={"msgspec": ["py.typed", "*.pyi"], "msgspec_x": ["py.typed", "*.pyi"]}, ext_modules=ext_modules, long_description=( open("README.md", encoding="utf-8").read() diff --git a/tests/test_struct_meta.py b/tests/test_struct_meta.py new file mode 100644 index 00000000..c7e03199 --- /dev/null +++ b/tests/test_struct_meta.py @@ -0,0 +1,379 @@ +"""Tests for the exposed StructMeta metaclass.""" + +import pytest +import msgspec +from msgspec import Struct, StructMeta +from msgspec.structs import asdict, astuple, replace, force_setattr + + +def test_struct_meta_exists(): + """Test that StructMeta is properly exposed.""" + assert hasattr(msgspec, "StructMeta") + assert isinstance(Struct, StructMeta) + assert issubclass(StructMeta, type) + + +def test_struct_meta_direct_usage(): + """Test that StructMeta can be used directly as a metaclass.""" + + class CustomStruct(metaclass=StructMeta): + x: int + y: str + + # Verify the struct works as expected + instance = CustomStruct(x=1, y="test") + assert instance.x == 1 + assert instance.y == "test" + assert isinstance(instance, CustomStruct) + assert isinstance(CustomStruct, StructMeta) + + +def test_struct_meta_options(): + """Test that StructMeta properly handles struct options.""" + + class CustomStruct(metaclass=StructMeta, frozen=True): + x: int + + # Verify options were applied + instance = CustomStruct(x=1) + with pytest.raises(AttributeError): + instance.x = 2 # Should be frozen + + +def test_struct_meta_field_processing(): + """Test that StructMeta properly processes fields.""" + + class CustomStruct(metaclass=StructMeta): + x: int + y: str = "default" + + # Verify struct functionality + instance = CustomStruct(x=1) + assert instance.x == 1 + assert instance.y == "default" + + # Check struct metadata + assert hasattr(CustomStruct, "__struct_fields__") + assert "x" in CustomStruct.__struct_fields__ + assert "y" in CustomStruct.__struct_fields__ + + +def test_struct_meta_with_struct_base(): + """Test using StructMeta with Struct as a base class.""" + + class CustomStruct(Struct): + x: int + y: str + + # Verify the struct works as expected + instance = CustomStruct(x=1, y="test") + assert instance.x == 1 + assert instance.y == "test" + assert isinstance(instance, CustomStruct) + assert isinstance(CustomStruct, StructMeta) + + +def test_struct_meta_validation(): + """Test that StructMeta validation works.""" + # Should raise TypeError for invalid field name + with pytest.raises(TypeError): + + class InvalidStruct(metaclass=StructMeta): + __dict__: int # __dict__ is a reserved name + + +def test_struct_meta_with_options(): + """Test StructMeta with various options.""" + + class Point(metaclass=StructMeta, frozen=True, eq=True, order=True): + x: int + y: int + + p1 = Point(x=1, y=2) + p2 = Point(x=1, y=3) + + # Test frozen + with pytest.raises(AttributeError): + p1.x = 10 + + # Test eq - note that we need to compare fields manually + # since equality is based on identity by default + assert p1.x == Point(x=1, y=2).x and p1.y == Point(x=1, y=2).y + assert p1.x == p2.x and p1.y != p2.y + + # Test order - we can't directly compare instances + # but we can compare their field values + assert (p1.x, p1.y) < (p2.x, p2.y) + + +def test_struct_meta_inheritance(): + """Test that StructMeta can be inherited in Python code.""" + + class CustomMeta(StructMeta): + """A custom metaclass that inherits from StructMeta. + + This metaclass adds a kw_only_default parameter that can be used to + set the default kw_only value for all subclasses. + + When a class is created with this metaclass: + 1. If kw_only is explicitly specified, use that value + 2. If kw_only is not specified but kw_only_default is, use kw_only_default + 3. If neither is specified but a parent class has kw_only_default defined, + use the parent's kw_only_default + 4. Otherwise, default to False + """ + + # Class attribute to store kw_only_default settings for each class + _kw_only_default_settings = {} + + def __new__(mcls, name, bases, namespace, **kwargs): + # Check if kw_only is explicitly specified + kw_only_specified = "kw_only" in kwargs + + # Process kw_only_default parameter + kw_only_default = kwargs.pop("kw_only_default", None) + + # If kw_only_default is specified, store it + if kw_only_default is not None: + # Remember this setting for future subclasses + mcls._kw_only_default_settings[name] = kw_only_default + else: + # Check if any parent class has kw_only_default defined + for base in bases: + base_name = base.__name__ + if base_name in mcls._kw_only_default_settings: + # Use parent's kw_only_default + kw_only_default = mcls._kw_only_default_settings[base_name] + break + + # If kw_only is not specified but kw_only_default is available, use it + if not kw_only_specified and kw_only_default is not None: + kwargs["kw_only"] = kw_only_default + + # Create the class + cls = super().__new__(mcls, name, bases, namespace, **kwargs) + return cls + + # Test basic functionality - without kw_only_default + class SimpleModel(metaclass=CustomMeta): + x: int + y: str + + # Verify the class was created correctly + assert isinstance(SimpleModel, CustomMeta) + assert issubclass(CustomMeta, StructMeta) + + # Test creating an instance with positional arguments (should work) + instance = SimpleModel(1, "test") + assert instance.x == 1 + assert instance.y == "test" + + # Test setting kw_only_default=True + class KwOnlyBase(metaclass=CustomMeta, kw_only_default=True): + """Base class that sets kw_only_default=True""" + + pass + + # Test a simple child class, should inherit kw_only_default + class SimpleChild(KwOnlyBase): + x: int + + # Should only allow keyword arguments + with pytest.raises(TypeError): + SimpleChild(1) + + class BadFieldOrder(KwOnlyBase): + x: int = 0 + y: int + + BadFieldOrder(y=10) + + # Create instance with keyword arguments + child = SimpleChild(x=1) + assert child.x == 1 + + # Test overriding inherited kw_only_default + class NonKwOnlyChild(KwOnlyBase, kw_only=False): + x: int + + # Should allow positional arguments + non_kw_child = NonKwOnlyChild(1) + assert non_kw_child.x == 1 + + # Test independent class, not inheriting kw_only_default + class IndependentModel(metaclass=CustomMeta): + x: int + y: str + + # Should allow positional arguments + independent = IndependentModel(1, "test") + assert independent.x == 1 + assert independent.y == "test" + + # Print debug information + print( + f"KwOnlyBase in _kw_only_default_settings: {'KwOnlyBase' in CustomMeta._kw_only_default_settings}" + ) + print( + f"KwOnlyBase default: {CustomMeta._kw_only_default_settings.get('KwOnlyBase')}" + ) + print( + f"SimpleChild in _kw_only_default_settings: {'SimpleChild' in CustomMeta._kw_only_default_settings}" + ) + + # Test that kw_only_default values are correctly passed + assert "KwOnlyBase" in CustomMeta._kw_only_default_settings + assert CustomMeta._kw_only_default_settings["KwOnlyBase"] is True + + # Test asdict + d = asdict(independent) + assert d["x"] == 1 + assert d["y"] == "test" + + +def test_struct_meta_subclass_functions(): + """Test if structs created by StructMeta subclasses support various function operations.""" + + # Define a custom metaclass + class CustomMeta(StructMeta): + """Custom metaclass that inherits from StructMeta""" + + pass + + # Use the custom metaclass to create a struct class + class CustomStruct(metaclass=CustomMeta): + x: int + y: str + z: float = 3.14 + + # Create an instance + obj = CustomStruct(x=1, y="test") + assert obj.x == 1 + assert obj.y == "test" + assert obj.z == 3.14 + + # Test asdict function + d = asdict(obj) + assert isinstance(d, dict) + assert d["x"] == 1 + assert d["y"] == "test" + assert d["z"] == 3.14 + + # Test astuple function + t = astuple(obj) + assert isinstance(t, tuple) + assert t == (1, "test", 3.14) + + # Test replace function + obj2 = replace(obj, y="replaced") + assert obj2.x == 1 + assert obj2.y == "replaced" + assert obj2.z == 3.14 + + # Test force_setattr function + force_setattr(obj, "x", 100) + assert obj.x == 100 + + # Test nested structs + class NestedStruct(metaclass=CustomMeta): + inner: CustomStruct + name: str + + nested = NestedStruct(inner=obj, name="nested") + assert nested.inner.x == 100 + assert nested.inner.y == "test" + assert nested.name == "nested" + + # Test asdict with nested structs + nested_dict = asdict(nested) + assert isinstance(nested_dict, dict) + # Note: asdict doesn't recursively convert nested struct objects, so inner remains a CustomStruct object + assert isinstance(nested_dict["inner"], CustomStruct) + assert nested_dict["inner"].x == 100 + assert nested_dict["inner"].y == "test" + assert nested_dict["name"] == "nested" + + +def test_struct_meta_subclass_inheritance(): + """Test multi-level inheritance of StructMeta subclasses.""" + + # Define the first level custom metaclass + class BaseMeta(StructMeta): + """Base custom metaclass""" + + pass + + # Define the second level custom metaclass + class DerivedMeta(BaseMeta): + """Derived custom metaclass""" + + pass + + # Use the second level custom metaclass to create a struct class + class DerivedStruct(metaclass=DerivedMeta): + a: int + b: str + + # Create an instance + obj = DerivedStruct(a=42, b="derived") + assert obj.a == 42 + assert obj.b == "derived" + + # Test various functions + # asdict + d = asdict(obj) + assert d["a"] == 42 + assert d["b"] == "derived" + + # astuple + t = astuple(obj) + assert t == (42, "derived") + + # replace + obj2 = replace(obj, a=99) + assert obj2.a == 99 + assert obj2.b == "derived" + + +def test_struct_meta_subclass_with_encoder(): + """Test compatibility of structs created by StructMeta subclasses with encoders.""" + + # Define a custom metaclass + class EncoderMeta(StructMeta): + """Custom metaclass for testing encoders""" + + pass + + # Use the custom metaclass to create a struct class + class EncoderStruct(metaclass=EncoderMeta): + id: int + name: str + tags: list[str] = [] + + # Create an instance + obj = EncoderStruct(id=123, name="test") + + # Test JSON encoding and decoding + json_bytes = msgspec.json.encode(obj) + decoded = msgspec.json.decode(json_bytes, type=EncoderStruct) + + assert decoded.id == 123 + assert decoded.name == "test" + assert decoded.tags == [] + + # Test encoding and decoding with nested structs + class Container(metaclass=EncoderMeta): + item: EncoderStruct + count: int + + container = Container(item=obj, count=1) + json_bytes = msgspec.json.encode(container) + decoded = msgspec.json.decode(json_bytes, type=Container) + + assert decoded.count == 1 + assert decoded.item.id == 123 + assert decoded.item.name == "test" + + +if __name__ == "__main__": + pytest.main(["-xvs", __file__])