firstclass
@firstclass works like @dataclass (same keyword args) and adds conversion to and from
packed bytes, ctypes, a C struct header, JSON and XML. Pure Python 3.10+, no dependencies.
from firstclass import firstclass, uint8_t, uint16_t, int32_t, float32_t, bool_t, char_array, bytes_t, array_t
@firstclass
class Point:
x: int32_t
y: int32_t
@firstclass
class Packet:
kind: uint8_t
ok: bool_t
temp: float32_t
name: char_array[8] # NUL-padded UTF-8
mac: bytes_t[6] # exactly 6 bytes
samples: array_t[uint16_t, 4]
origin: Point # nested
p = Packet(1, True, 21.5, "sensor", b"\x00\x11\x22\x33\x44\x55", [1, 2, 3, 4], Point(10, -20))
raw = p.to_bytes() # packed, little-endian
assert Packet.from_bytes(raw) == p
Packet.sizeof() # 36
Types
| Marker | C | Bytes |
|---|---|---|
uint8_t uint16_t uint32_t uint64_t |
uintN_t |
1/2/4/8 |
int8_t int16_t int32_t int64_t |
intN_t |
1/2/4/8 |
float32_t / float64_t |
float / double |
4 / 8 |
bool_t |
bool |
1 |
char_t |
char |
1 |
char_array[N] |
char x[N] |
N (UTF-8, NUL-padded) |
bytes_t[N] |
uint8_t x[N] |
N |
array_t[T, N] |
T x[N] |
N * size(T) |
enum_t[Color, uint8_t] |
uint8_t x (+ enum Color {..} in header) |
storage size |
bitfield[uint8_t, 3] |
uint8_t x : 3 |
shares a storage unit |
another @firstclass class |
struct Name |
its size |
Values are checked: out of range ints, over-long strings and wrong lengths raise
ValueError; wrong Python types raise TypeError. Nothing is truncated or coerced silently.
Enums. Int-valued Enum/IntEnum/IntFlag classes. enum_t[Color, uint8_t] picks the width;
a bare Color annotation is 32-bit like a C int (unsigned unless a value is negative). Values
are stored, and written to JSON/XML, as ints; reading an unknown value raises ValueError.
c_header() emits enum Color { Color_RED = 1, ... };.
Bitfields. bitfield[uint8_t, 3] is 3 bits of a uint8_t unit, like uint8_t x : 3.
Neighbouring bitfields with the same storage type share a unit while they fit; otherwise a new
unit starts. Little-endian fills from the lowest bit, big-endian from the highest (what ctypes
does). Signed storage (int8_t) gives signed fields. Mixing storage types in one run is not
portable across C compilers; keep a run on one type. No arrays of bitfields.
Raw ctypes types work as annotations too (packed, same layout as a _pack_ = 1 Structure):
ctypes.c_int, c_uint64, c_size_t, c_bool, c_float, c_double, c_char, c_uint16 * 4,
c_char * 8. Pointers (c_void_p, POINTER(T), c_char_p) are stored as the address (an int,
0 = NULL) at pointer size; to_ctypes() gives a real pointer field you can dereference. The
address only means something inside the same process. Sizes follow the platform
(c_long is 4 bytes on Windows, 8 on Linux).
Methods added to every class
| Method | |
|---|---|
obj.to_bytes() / Cls.from_bytes(data) |
binary; from_bytes needs exactly sizeof() bytes |
Cls.unpack_from(buffer, offset=0) |
like struct.unpack_from: read one object at offset, trailing bytes are fine; any buffer type |
Cls.sizeof() |
size in bytes |
obj.to_dict() / Cls.from_dict(d) |
JSON-compatible dict (bytes as hex) |
obj.to_json(**kw) / Cls.from_json(s) |
kw goes to json.dumps |
obj.to_xml(indent=None) / Cls.from_xml(s) |
<Packet><kind>1</kind>...</Packet>, arrays as <item> |
obj.to_ctypes() / Cls.from_ctypes(c) / Cls.ctypes_type() |
ctypes.Structure with matching layout |
Cls.c_header() |
C source for the struct(s), nested ones first |
Import C/C++ headers
from firstclass import from_header # also: firstclass.from_header
t = from_header("protocol.h") # or from_header(text="struct P { ... };")
pkt = t.Packet(1, True, ...) # structs -> @firstclass classes
t.Color.RED # enums / enum class -> IntEnum
t.constants["MAX_LEN"] # integer #defines, consts, enum members
t.skipped # {"name": "reason"} for anything not convertible
Understood: struct/class/typedef struct, nested and anonymous-member structs, enum and
enum class : type (explicit values, implicit increments, constant expressions), bitfields, arrays
(multi-dimensional, sizes from #define/enum/const/sizeof), char[N] -> char_array,
uint8_t[N] -> bytes_t, pointers and function pointers (stored as addresses), typedef/using,
#define/#if/#ifdef/#elif (preset macros with defines={...}), namespace, extern "C",
#pragma pack(push/pop/N) and __attribute__((packed)). Layout follows C: natural alignment
padding unless the struct is packed; force it with align=True/False. Options: byteorder,
long_size (size of C long; default is this machine's).
Skipped with a reason instead of failing: unions, flexible array members, unnamed padding
bitfields, inheritance, virtual members, unknown types (#included headers are not read), pack(2)
/pack(4), aligned(N), long double, empty structs, anything depending on a skipped struct.
Plain C enums are 32-bit like C unless : type is given. C field names that are Python keywords
(class, from, ...) get a trailing underscore (class_).
Comparison
Like a dataclass, == / != compare field by field. Unlike a plain dataclass, ordering is on by
default: <, <=, >, >= compare fields in declaration order, like tuples, so you can sorted(),
min() and max() instances. Comparing with another class raises TypeError (== is just False).
Switch it off with @firstclass(order=False); eq=False turns off both.
Layout options
@firstclass(byteorder="big") # default "little"
@firstclass(align=True) # default False = packed, no padding (like __attribute__((packed)))
@firstclass(frozen=True, slots=True) # any dataclass option
align=True inserts natural-alignment padding like a normal C struct (assumes the x86-64 style
rule: alignment equals size). Padding bytes are written as zero and ignored on read.
C headers describe the struct only; C compilers use native byte order, so byteorder is noted
in a comment.
Notes
- Fields with plain types (
str,int) work for JSON/XML only;to_bytes()raisesTypeErrornaming the field. - Use
from __future__ import annotationsfreely; annotations are resolved lazily. Classes defined inside functions that refer to local names are not resolvable. - Don't parse untrusted XML with
from_xmlwithout extra care (stdlibxml.etree). - Field names that collide with the generated methods (
to_bytes, ...) raiseTypeError.
Example
python examples/example.py defines a class in Python, then imports examples/protocol.h (enums, enum class,
bitfields, #pragma pack structs) and uses the generated classes.
Develop
python -m unittest discover -s tests
Metadata
Release files for firstclass 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| firstclass-0.1.0.tar.gz | 47.5 kB | Without provenance | Details for firstclass-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| firstclass-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for firstclass-0.1.0-py3-none-any.whl |
Total release size: 72.0 kB
Release files / firstclass-0.1.0.tar.gz
| Download URL | firstclass-0.1.0.tar.gz |
|---|---|
| Size | 47.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ff4d1158034c1f9ce75d7ac7221955f3c65ce7fe2fb92641845effd020d61d09
|
|
BLAKE2b-256 checksum How to use checksums |
417abace54865176b863fef6731cdee7ebb72314be428c30bca6dba3eb5cc640
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / firstclass-0.1.0-py3-none-any.whl
| Download URL | firstclass-0.1.0-py3-none-any.whl |
|---|---|
| Size | 24.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
679d45d71f928a15f8435ee315808a79b52b2a5c3c3df42d9d3465a4343632ae
|
|
BLAKE2b-256 checksum How to use checksums |
4f45dab489b1a95b63492cbedebd651ac3be58743663ae907129ce62556b07d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for firstclass 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.