Compare commits

...

22 Commits
master ... next

Author SHA1 Message Date
Georg K
87b84afc4b Merge branch 'rust_extras' into next 2023-09-06 14:34:14 +03:00
Erik Friese
8283ef7298 bugfix
map fields with values of message type were not serialized correctly
2023-09-05 21:58:58 +02:00
Erik Friese
0931eb3bf5 bugfix
byte fields were deserialized incorrectly
2023-09-05 20:26:46 +02:00
Erik Friese
8f535913a1 bugfix
using python identifier in message names
necessary to distinguish between dynamically created classes with same name
2023-09-05 20:06:00 +02:00
Erik Friese
fd02cb6180 supporting datetime and timedelta 2023-09-05 11:27:04 +02:00
Erik Friese
950d2f6536 google wrapper types 2023-09-04 21:09:06 +02:00
Erik Friese
29f12ea88d map support 2023-09-04 12:52:12 +02:00
Erik Friese
219233b50e bugfix: parsing unknown fields properly 2023-08-31 17:57:28 +02:00
Erik Friese
2d30bdb7b2 Merge branch 'master' into rust_extras 2023-08-31 17:39:34 +02:00
Erik Friese
bdd3389b17 bugfix in proto descriptor creation
reference cycles in betterproto messages have led to infinite recursion
2023-08-31 17:17:28 +02:00
Erik Friese
441844b97a avoiding name clash 2023-08-31 13:18:07 +02:00
Erik Friese
a413d08fc1 enum support 2023-08-30 21:06:32 +02:00
Erik Friese
24d694afe2 storing unknown fields 2023-08-30 15:49:25 +02:00
Erik Friese
84af157122 minor refactoring 2023-08-30 15:39:34 +02:00
Erik Friese
df0c17bf0a optional support 2023-08-29 18:08:59 +02:00
Erik Friese
d1825026db lock file reverted to master 2023-08-27 18:46:20 +02:00
Erik Friese
a12c9d24de oneof support 2023-08-27 14:37:23 +02:00
Erik Friese
d79a9eee14 deserializing lists 2023-08-27 13:22:51 +02:00
Erik Friese
d848d05710 minor optimization 2023-08-26 21:47:35 +02:00
Erik Friese
26da86d2cd proper error handling 2023-08-26 21:32:35 +02:00
Erik Friese
604dcb104f type info + doc string added 2023-08-26 21:04:30 +02:00
Erik Friese
421aa78014 Native deserialization based on Rust and PyO3
Proof of concept
Only capable of deserializing (nested) Messages with primitive fields
No handling of lists, maps, enums, .. implemented yet
See `example.py` for a working example
2023-08-26 13:04:26 +02:00
15 changed files with 1163 additions and 86 deletions

72
betterproto-extras/.gitignore vendored Normal file
View File

@ -0,0 +1,72 @@
/target
# Byte-compiled / optimized / DLL files
__pycache__/
.pytest_cache/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
.venv/
env/
bin/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
include/
man/
venv/
*.egg-info/
.installed.cfg
*.egg
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
pip-selfcheck.json
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Rope
.ropeproject
# Django stuff:
*.log
*.pot
.DS_Store
# Sphinx documentation
docs/_build/
# PyCharm
.idea/
# VSCode
.vscode/
# Pyenv
.python-version

383
betterproto-extras/Cargo.lock generated Normal file
View File

@ -0,0 +1,383 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "betterproto-extras"
version = "0.1.0"
dependencies = [
"indoc 2.0.3",
"prost-reflect",
"pyo3",
"thiserror",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "indoc"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
[[package]]
name = "indoc"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c785eefb63ebd0e33416dfcb8d6da0bf27ce752843a45632a67bf10d4d4b5c4"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "lock_api"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "memoffset"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "proc-macro2"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "prost"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-derive"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "prost-reflect"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b823de344848e011658ac981009100818b322421676740546f8b52ed5249428"
dependencies = [
"once_cell",
"prost",
"prost-types",
]
[[package]]
name = "prost-types"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13"
dependencies = [
"prost",
]
[[package]]
name = "pyo3"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38"
dependencies = [
"cfg-if",
"indoc 1.0.9",
"libc",
"memoffset",
"parking_lot",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-build-config"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5"
dependencies = [
"once_cell",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn 1.0.109",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "smallvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-lexicon"
version = "0.12.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a"
[[package]]
name = "thiserror"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]]
name = "unicode-ident"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
[[package]]
name = "unindent"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c"
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"

View File

@ -0,0 +1,14 @@
[package]
name = "betterproto-extras"
version = "0.1.0"
edition = "2021"
[lib]
name = "betterproto_extras"
crate-type = ["cdylib"]
[dependencies]
indoc = "2.0.3"
prost-reflect = "0.11.5"
pyo3 = { version = "0.19.2", features = ["abi3-py37", "extension-module"] }
thiserror = "1.0.47"

View File

@ -0,0 +1,5 @@
def deserialize(msg, data: bytes):
"""
Parses the binary encoded Protobuf `data` with respect to the metadata
given by the betterproto message `msg`, and merges the result into `msg`.
"""

View File

@ -0,0 +1,16 @@
[build-system]
requires = ["maturin>=1.2,<2.0"]
build-backend = "maturin"
[project]
name = "betterproto-extras"
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
[tool.maturin]
features = ["pyo3/extension-module"]

View File

@ -0,0 +1,289 @@
use crate::{
error::{Error, Result},
py_any_extras::PyAnyExtras,
};
use prost_reflect::{
prost_types::{
field_descriptor_proto::{Label, Type},
DescriptorProto, EnumDescriptorProto, EnumValueDescriptorProto, FieldDescriptorProto,
FileDescriptorProto, MessageOptions, OneofDescriptorProto,
},
DescriptorPool, MessageDescriptor,
};
use pyo3::PyAny;
use std::sync::{Mutex, OnceLock};
pub fn create_cached_descriptor(obj: &PyAny) -> Result<MessageDescriptor> {
static DESCRIPTOR_POOL: OnceLock<Mutex<DescriptorPool>> = OnceLock::new();
let mut pool = DESCRIPTOR_POOL
.get_or_init(|| Mutex::new(DescriptorPool::global()))
.lock()
.unwrap();
let cls = obj.getattr("__class__")?;
let name = format!("{}_{}", cls.qualified_name()?, cls.py_identifier());
if let Some(desc) = pool.get_message_by_name(&name) {
return Ok(desc);
}
let mut file = FileDescriptorProto {
name: Some(name.clone()),
..Default::default()
};
add_message_to_file(name.clone(), obj, &pool, &mut file)?;
pool.add_file_descriptor_proto(file)?;
Ok(pool.get_message_by_name(&name).expect("Just registered..."))
}
fn add_message_to_file(
message_name: String,
obj: &PyAny,
pool: &DescriptorPool,
file: &mut FileDescriptorProto,
) -> Result<()> {
let mut messages_to_add = vec![(message_name, obj)];
while let Some((message_name, obj)) = messages_to_add.pop() {
let meta = obj.get_proto_meta()?;
let mut message = DescriptorProto {
name: Some(message_name.to_string()),
..Default::default()
};
for item in meta
.getattr("meta_by_field_name")?
.call_method0("items")?
.iter()?
{
let (field_name, field_meta) = item?.extract::<(&str, &PyAny)>()?;
message.field.push({
let mut field = FieldDescriptorProto {
name: Some(field_name.to_string()),
number: Some(field_meta.getattr("number")?.extract::<i32>()?),
..Default::default()
};
let proto_type = field_meta.getattr("proto_type")?.extract::<&str>()?;
if proto_type == "map" {
field.set_type(Type::Message);
let (key, val) = field_meta.getattr("map_types")?.extract::<(&str, &str)>()?;
let key = map_type(key)?;
let val = map_type(val)?;
if matches!(
key,
Type::Float | Type::Double | Type::Bytes | Type::Message | Type::Enum
) {
return Err(Error::UnsupportedMapKeyType(key));
}
let map_entry_name = format!("{field_name}Entry");
field.type_name = Some(format!("{message_name}.{map_entry_name}"));
field.set_label(Label::Repeated);
message.nested_type.push(DescriptorProto {
name: Some(map_entry_name),
field: vec![
{
let mut proto = FieldDescriptorProto {
name: Some("key".to_string()),
number: Some(1),
..Default::default()
};
proto.set_type(key);
proto
},
{
let mut proto = FieldDescriptorProto {
name: Some("value".to_string()),
number: Some(2),
..Default::default()
};
proto.set_type(val);
if val == Type::Message {
set_type_name(
&message_name,
meta.get_class(&format!("{field_name}.value"))?,
&mut proto,
file,
&mut messages_to_add,
pool,
)?;
}
proto
},
],
options: Some(MessageOptions {
map_entry: Some(true),
..Default::default()
}),
..Default::default()
})
} else {
field.set_type(map_type(proto_type)?);
match field.r#type() {
Type::Message => match field_meta
.getattr("wraps")?
.extract::<Option<&str>>()?
.map(map_type)
.transpose()?
{
Some(Type::Bool) => {
field.type_name = Some("google.protobuf.BoolValue".to_string());
}
Some(Type::Double) => {
field.type_name = Some("google.protobuf.DoubleValue".to_string());
}
Some(Type::Float) => {
field.type_name = Some("google.protobuf.FloatValue".to_string());
}
Some(Type::Int64) => {
field.type_name = Some("google.protobuf.Int64Value".to_string());
}
Some(Type::Uint64) => {
field.type_name = Some("google.protobuf.UInt64Value".to_string());
}
Some(Type::Int32) => {
field.type_name = Some("google.protobuf.Int32Value".to_string());
}
Some(Type::Uint32) => {
field.type_name = Some("google.protobuf.UInt32Value".to_string());
}
Some(Type::String) => {
field.type_name = Some("google.protobuf.StringValue".to_string());
}
Some(Type::Bytes) => {
field.type_name = Some("google.protobuf.BytesValue".to_string());
}
Some(t) => return Err(Error::UnsupportedWrapperType(t)),
None => {
set_type_name(
&message_name,
meta.get_class(field_name)?,
&mut field,
file,
&mut messages_to_add,
pool,
)?;
}
},
Type::Enum => {
let cls = meta.get_class(field_name)?;
let cls_name =
format!("{}_{}", cls.qualified_name()?, cls.py_identifier());
field.type_name = Some(cls_name.to_string());
if pool.get_enum_by_name(&cls_name).is_none()
&& !file.enum_type.iter().any(|item| item.name() == cls_name)
{
let mut proto = EnumDescriptorProto {
name: Some(cls_name.clone()),
..Default::default()
};
for item in cls.iter()? {
let item = item?;
proto.value.push(EnumValueDescriptorProto {
number: Some(item.getattr("value")?.extract()?),
name: Some(format!(
"{}_{}",
cls_name,
item.getattr("name")?.extract::<&str>()?
)),
..Default::default()
});
}
file.enum_type.push(proto);
}
}
_ => {}
}
if meta.is_list_field(field_name)? {
field.set_label(Label::Repeated);
} else if field_meta.getattr("optional")?.extract::<bool>()? {
field.proto3_optional = Some(true);
}
}
if let Some(grp) = meta.oneof_group(field_name)? {
let oneof_index = message.oneof_decl.iter().position(|x| x.name() == grp);
match oneof_index {
Some(i) => field.oneof_index = Some(i as i32),
None => {
message.oneof_decl.push(OneofDescriptorProto {
name: Some(grp),
..Default::default()
});
field.oneof_index = Some((message.oneof_decl.len() - 1) as i32)
}
}
}
field
});
}
file.message_type.push(message);
}
Ok(())
}
fn map_type(str: &str) -> Result<Type> {
match str {
"enum" => Ok(Type::Enum),
"bool" => Ok(Type::Bool),
"int32" => Ok(Type::Int32),
"int64" => Ok(Type::Int64),
"uint32" => Ok(Type::Uint32),
"uint64" => Ok(Type::Uint64),
"sint32" => Ok(Type::Sint32),
"sint64" => Ok(Type::Sint64),
"float" => Ok(Type::Float),
"double" => Ok(Type::Double),
"fixed32" => Ok(Type::Fixed32),
"sfixed32" => Ok(Type::Sfixed32),
"fixed64" => Ok(Type::Fixed64),
"sfixed64" => Ok(Type::Sfixed64),
"string" => Ok(Type::String),
"bytes" => Ok(Type::Bytes),
"message" => Ok(Type::Message),
_ => Err(Error::UnsupportedType(str.to_string())),
}
}
fn set_type_name<'py>(
message_name: &str,
field_cls: &'py PyAny,
field: &mut FieldDescriptorProto,
file: &FileDescriptorProto,
messages_to_add: &mut Vec<(String, &'py PyAny)>,
pool: &DescriptorPool,
) -> Result<()> {
let cls_name = field_cls.qualified_name()?;
match cls_name.as_str() {
"datetime.datetime" => {
field.type_name = Some("google.protobuf.Timestamp".to_string());
}
"datetime.timedelta" => {
field.type_name = Some("google.protobuf.Duration".to_string());
}
_ => {
let cls_name = format!("{}_{}", cls_name, field_cls.py_identifier());
field.type_name = Some(cls_name.clone());
if message_name != cls_name
&& pool.get_message_by_name(&cls_name).is_none()
&& !file.message_type.iter().any(|item| item.name() == cls_name)
&& !messages_to_add.iter().any(|item| item.0 == cls_name)
{
messages_to_add.push((cls_name, field_cls.call0()?));
}
}
}
Ok(())
}

View File

@ -0,0 +1,29 @@
use prost_reflect::{
prost::DecodeError, prost_types::field_descriptor_proto::Type, DescriptorError,
};
use pyo3::{exceptions::PyRuntimeError, PyErr};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("Given object is not a valid betterproto message.")]
NoBetterprotoMessage(#[from] PyErr),
#[error("Unsupported type `{0}`.")]
UnsupportedType(String),
#[error("Unsupported map key type `{0:?}`.")]
UnsupportedMapKeyType(Type),
#[error("Unsupported wrapper type `{0:?}`.")]
UnsupportedWrapperType(Type),
#[error("Error on proto registration")]
FailedToRegisterDescriptor(#[from] DescriptorError),
#[error("The given binary data does not match the protobuf schema.")]
FailedToDecode(#[from] DecodeError),
}
pub type Result<T> = core::result::Result<T, Error>;
impl From<Error> for PyErr {
fn from(value: Error) -> Self {
PyRuntimeError::new_err(value.to_string())
}
}

View File

@ -0,0 +1,24 @@
mod descriptor_pool;
mod error;
mod merging;
mod py_any_extras;
use descriptor_pool::create_cached_descriptor;
use error::Result;
use merging::merge_msg_into_pyobj;
use prost_reflect::DynamicMessage;
use pyo3::prelude::*;
#[pyfunction]
fn deserialize(obj: &PyAny, buf: &[u8]) -> Result<()> {
let desc = create_cached_descriptor(obj)?;
let msg = DynamicMessage::decode(desc, buf)?;
merge_msg_into_pyobj(obj, msg)?;
Ok(())
}
#[pymodule]
fn betterproto_extras(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(deserialize, m)?)?;
Ok(())
}

View File

@ -0,0 +1,182 @@
use crate::{error::Result, py_any_extras::PyAnyExtras};
use indoc::indoc;
use prost_reflect::{
prost_types::{Duration, Timestamp},
DynamicMessage, MapKey, ReflectMessage, Value,
};
use pyo3::{
sync::GILOnceCell,
types::{IntoPyDict, PyBytes, PyModule},
Py, PyAny, PyObject, Python, ToPyObject,
};
pub fn merge_msg_into_pyobj(obj: &PyAny, mut msg: DynamicMessage) -> Result<()> {
for field in msg.take_fields() {
let field_name = field.0.name();
let proto_meta = obj.get_proto_meta()?;
obj.setattr(
field_name,
map_field_value(field_name, field.1, proto_meta)?,
)?;
}
let mut buf = vec![];
for field in msg.unknown_fields() {
field.encode(&mut buf);
}
if !buf.is_empty() {
let mut unknown_fields = obj.getattr("_unknown_fields")?.extract::<Vec<u8>>()?;
unknown_fields.append(&mut buf);
obj.setattr("_unknown_fields", PyBytes::new(obj.py(), &unknown_fields))?;
}
obj.setattr("_serialized_on_wire", true)?;
Ok(())
}
fn map_field_value(field_name: &str, field_value: Value, proto_meta: &PyAny) -> Result<PyObject> {
let py = proto_meta.py();
match field_value {
Value::Bool(x) => Ok(x.to_object(py)),
Value::Bytes(x) => Ok(PyBytes::new(py, &x).to_object(py)),
Value::F32(x) => Ok(x.to_object(py)),
Value::F64(x) => Ok(x.to_object(py)),
Value::I32(x) => Ok(x.to_object(py)),
Value::I64(x) => Ok(x.to_object(py)),
Value::String(x) => Ok(x.to_object(py)),
Value::U32(x) => Ok(x.to_object(py)),
Value::U64(x) => Ok(x.to_object(py)),
Value::Message(msg) => match msg.descriptor().full_name() {
"google.protobuf.BoolValue" => Ok(msg
.get_field_by_number(1)
.and_then(|val| val.as_bool())
.to_object(py)),
"google.protobuf.DoubleValue" => Ok(msg
.get_field_by_number(1)
.and_then(|val| val.as_f64())
.to_object(py)),
"google.protobuf.FloatValue" => Ok(msg
.get_field_by_number(1)
.and_then(|val| val.as_f32())
.to_object(py)),
"google.protobuf.Int64Value" => Ok(msg
.get_field_by_number(1)
.and_then(|val| val.as_i64())
.to_object(py)),
"google.protobuf.UInt64Value" => Ok(msg
.get_field_by_number(1)
.and_then(|val| val.as_u64())
.to_object(py)),
"google.protobuf.Int32Value" => Ok(msg
.get_field_by_number(1)
.and_then(|val| val.as_i32())
.to_object(py)),
"google.protobuf.UInt32Value" => Ok(msg
.get_field_by_number(1)
.and_then(|val| val.as_u32())
.to_object(py)),
"google.protobuf.StringValue" => Ok(msg
.get_field_by_number(1)
.and_then(|val| val.as_str().map(|s| s.to_string()))
.to_object(py)),
"google.protobuf.BytesValue" => Ok(msg
.get_field_by_number(1)
.and_then(|val| val.as_bytes().map(|b| PyBytes::new(py, b)))
.to_object(py)),
"google.protobuf.Timestamp" => {
let msg = msg.transcode_to::<Timestamp>()?;
Ok(create_py_datetime(&msg, py))
}
"google.protobuf.Duration" => {
let msg = msg.transcode_to::<Duration>()?;
Ok(create_py_timedelta(&msg, py))
}
_ => {
let obj = proto_meta.create_instance(field_name)?;
merge_msg_into_pyobj(obj, msg)?;
Ok(obj.to_object(py))
}
},
Value::List(ls) => Ok(ls
.into_iter()
.map(|x| map_field_value(field_name, x, proto_meta))
.collect::<Result<Vec<PyObject>>>()?
.to_object(py)),
Value::EnumNumber(x) => {
let cls = proto_meta.get_class(field_name)?;
Ok(cls.call1((x,))?.to_object(py))
}
Value::Map(map) => {
let res: Result<Vec<_>> = map
.into_iter()
.map(|(k, v)| {
let key = map_key(k, py);
let val = map_field_value(&format!("{field_name}.value"), v, proto_meta)?;
Ok((key, val))
})
.collect();
Ok(res?.into_py_dict(py).to_object(py))
}
}
}
fn map_key(key: MapKey, py: Python) -> PyObject {
match key {
MapKey::Bool(x) => x.to_object(py),
MapKey::I32(x) => x.to_object(py),
MapKey::I64(x) => x.to_object(py),
MapKey::U32(x) => x.to_object(py),
MapKey::U64(x) => x.to_object(py),
MapKey::String(x) => x.to_object(py),
}
}
fn create_py_datetime(ts: &Timestamp, py: Python) -> PyObject {
static CONSTRUCTOR_CACHE: GILOnceCell<Py<PyAny>> = GILOnceCell::new();
let constructor = CONSTRUCTOR_CACHE.get_or_init(py, || {
let constructor = PyModule::from_code(
py,
indoc! {"
from datetime import datetime, timezone
def constructor(ts):
return datetime.fromtimestamp(ts, tz=timezone.utc)
"},
"",
"",
)
.expect("This is a valid Python module")
.getattr("constructor")
.expect("Attribute exists");
Py::from(constructor)
});
let ts = (ts.seconds as f64) + (ts.nanos as f64) / 1e9;
constructor
.call1(py, (ts,))
.expect("static function will not fail")
}
fn create_py_timedelta(duration: &Duration, py: Python) -> PyObject {
static CONSTRUCTOR_CACHE: GILOnceCell<Py<PyAny>> = GILOnceCell::new();
let constructor = CONSTRUCTOR_CACHE.get_or_init(py, || {
let constructor = PyModule::from_code(
py,
indoc! {"
from datetime import timedelta
def constructor(s, ms):
return timedelta(seconds=s, microseconds=ms)
"},
"",
"",
)
.expect("This is a valid Python module")
.getattr("constructor")
.expect("Attribute exists");
Py::from(constructor)
});
constructor
.call1(py, (duration.seconds as f64, (duration.nanos as f64) / 1e3))
.expect("static function will not fail")
}

View File

@ -0,0 +1,68 @@
use crate::error::Result;
use pyo3::{PyAny, Py, sync::GILOnceCell};
pub trait PyAnyExtras {
fn qualified_name(&self) -> Result<String>;
fn qualified_class_name(&self) -> Result<String>;
fn get_proto_meta(&self) -> Result<&PyAny>;
fn get_class(&self, field_name: &str) -> Result<&PyAny>;
fn create_instance(&self, field_name: &str) -> Result<&PyAny>;
fn is_list_field(&self, field_name: &str) -> Result<bool>;
fn oneof_group(&self, field_name: &str) -> Result<Option<String>>;
fn py_identifier(&self) -> u64;
}
impl PyAnyExtras for PyAny {
fn qualified_name(&self) -> Result<String> {
let module = self.getattr("__module__")?;
let name = self.getattr("__name__")?;
Ok(format!("{module}.{name}"))
}
fn qualified_class_name(&self) -> Result<String> {
self.getattr("__class__")?.qualified_name()
}
fn get_proto_meta(&self) -> Result<&PyAny> {
Ok(self.getattr("_betterproto")?)
}
fn get_class(&self, field_name: &str) -> Result<&PyAny> {
let cls = self.getattr("cls_by_field")?.get_item(field_name)?;
Ok(cls)
}
fn create_instance(&self, field_name: &str) -> Result<&PyAny> {
Ok(self.get_class(field_name)?.call0()?)
}
fn is_list_field(&self, field_name: &str) -> Result<bool> {
let cls = self.getattr("default_gen")?.get_item(field_name)?;
let module = cls.getattr("__module__")?;
let name = cls.getattr("__name__")?;
Ok(module.to_string() == "builtins" && name.to_string() == "list")
}
fn oneof_group(&self, field_name: &str) -> Result<Option<String>> {
let opt = self
.getattr("oneof_group_by_field")?
.call_method1("get", (field_name,))?
.extract()?;
Ok(opt)
}
fn py_identifier(&self) -> u64 {
static FUN_CACHE: GILOnceCell<Py<PyAny>> = GILOnceCell::new();
let py = self.py();
let fun = FUN_CACHE.get_or_init(py, || {
let fun = py
.eval("id", None, None)
.expect("This is a valid Python expression");
Py::from(fun)
});
fun.call1(py, (self,))
.expect("Identity function is callable")
.extract::<u64>(py)
.expect("Identity function always returns an integer")
}
}

55
example.py Normal file
View File

@ -0,0 +1,55 @@
# dev tests
# to be deleted later
import betterproto
from dataclasses import dataclass
from typing import Dict, List, Optional
@dataclass(repr=False)
class Baz(betterproto.Message):
a: float = betterproto.float_field(1, group = "x")
b: int = betterproto.int64_field(2, group = "x")
c: float = betterproto.float_field(3, group = "y")
d: int = betterproto.int64_field(4, group = "y")
e: Optional[int] = betterproto.int32_field(5, group = "_e", optional = True)
@dataclass(repr=False)
class Foo(betterproto.Message):
x: int = betterproto.int32_field(1)
y: float = betterproto.double_field(2)
z: List[Baz] = betterproto.message_field(3)
class Enm(betterproto.Enum):
A = 0
B = 1
C = 2
@dataclass(repr=False)
class Bar(betterproto.Message):
foo1: Foo = betterproto.message_field(1)
foo2: Foo = betterproto.message_field(2)
packed: List[int] = betterproto.int64_field(3)
enm: Enm = betterproto.enum_field(4)
map: Dict[int, bool] = betterproto.map_field(5, betterproto.TYPE_INT64, betterproto.TYPE_BOOL)
maybe: Optional[bool] = betterproto.message_field(6, wraps=betterproto.TYPE_BOOL)
bts: bytes = betterproto.bytes_field(7)
# Serialization has not been changed yet. So nothing unusual here
buffer = bytes(
Bar(
foo1=Foo(1, 2.34),
foo2=Foo(3, 4.56, [Baz(a = 1.234), Baz(b = 5, e=1), Baz(b = 2, d = 3)]),
packed=[5, 3, 1],
enm=Enm.B,
map={
1: True,
42: False
},
maybe=True,
bts=b'Hi There!'
)
)
# Native deserialization happening here
bar = Bar().parse(buffer)
print(bar)

98
poetry.lock generated
View File

@ -1,10 +1,9 @@
# This file is automatically @generated by Poetry and should not be changed by hand. # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]] [[package]]
name = "alabaster" name = "alabaster"
version = "0.7.13" version = "0.7.13"
description = "A configurable sidebar-enabled Sphinx theme" description = "A configurable sidebar-enabled Sphinx theme"
category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
@ -16,7 +15,6 @@ files = [
name = "ansicon" name = "ansicon"
version = "1.89.0" version = "1.89.0"
description = "Python wrapper for loading Jason Hood's ANSICON" description = "Python wrapper for loading Jason Hood's ANSICON"
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -28,7 +26,6 @@ files = [
name = "asv" name = "asv"
version = "0.4.2" version = "0.4.2"
description = "Airspeed Velocity: A simple Python history benchmarking tool" description = "Airspeed Velocity: A simple Python history benchmarking tool"
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [ files = [
@ -45,7 +42,6 @@ hg = ["python-hglib (>=1.5)"]
name = "atomicwrites" name = "atomicwrites"
version = "1.4.1" version = "1.4.1"
description = "Atomic file writes." description = "Atomic file writes."
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [ files = [
@ -56,7 +52,6 @@ files = [
name = "attrs" name = "attrs"
version = "23.1.0" version = "23.1.0"
description = "Classes Without Boilerplate" description = "Classes Without Boilerplate"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -78,7 +73,6 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
name = "babel" name = "babel"
version = "2.12.1" version = "2.12.1"
description = "Internationalization utilities" description = "Internationalization utilities"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -93,7 +87,6 @@ pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""}
name = "backports-cached-property" name = "backports-cached-property"
version = "1.0.2" version = "1.0.2"
description = "cached_property() - computed once per instance, cached as attribute" description = "cached_property() - computed once per instance, cached as attribute"
category = "dev"
optional = false optional = false
python-versions = ">=3.6.0" python-versions = ">=3.6.0"
files = [ files = [
@ -101,11 +94,23 @@ files = [
{file = "backports.cached_property-1.0.2-py3-none-any.whl", hash = "sha256:baeb28e1cd619a3c9ab8941431fe34e8490861fb998c6c4590693d50171db0cc"}, {file = "backports.cached_property-1.0.2-py3-none-any.whl", hash = "sha256:baeb28e1cd619a3c9ab8941431fe34e8490861fb998c6c4590693d50171db0cc"},
] ]
[[package]]
name = "betterproto-extras"
version = "0.1.0"
description = ""
optional = false
python-versions = ">=3.7"
files = []
develop = false
[package.source]
type = "directory"
url = "betterproto-extras"
[[package]] [[package]]
name = "black" name = "black"
version = "23.3.0" version = "23.3.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "main"
optional = true optional = true
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -156,7 +161,6 @@ uvloop = ["uvloop (>=0.15.2)"]
name = "blessed" name = "blessed"
version = "1.20.0" version = "1.20.0"
description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities."
category = "dev"
optional = false optional = false
python-versions = ">=2.7" python-versions = ">=2.7"
files = [ files = [
@ -173,7 +177,6 @@ wcwidth = ">=0.1.4"
name = "bpython" name = "bpython"
version = "0.19" version = "0.19"
description = "Fancy Interface to the Python Interpreter" description = "Fancy Interface to the Python Interpreter"
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -197,7 +200,6 @@ watch = ["watchdog"]
name = "certifi" name = "certifi"
version = "2023.5.7" version = "2023.5.7"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
@ -209,7 +211,6 @@ files = [
name = "cfgv" name = "cfgv"
version = "3.3.1" version = "3.3.1"
description = "Validate configuration and produce human readable error messages." description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false optional = false
python-versions = ">=3.6.1" python-versions = ">=3.6.1"
files = [ files = [
@ -221,7 +222,6 @@ files = [
name = "charset-normalizer" name = "charset-normalizer"
version = "3.1.0" version = "3.1.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "dev"
optional = false optional = false
python-versions = ">=3.7.0" python-versions = ">=3.7.0"
files = [ files = [
@ -306,7 +306,6 @@ files = [
name = "click" name = "click"
version = "8.1.3" version = "8.1.3"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
category = "main"
optional = true optional = true
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -322,7 +321,6 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
category = "main"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [ files = [
@ -334,7 +332,6 @@ files = [
name = "coverage" name = "coverage"
version = "7.2.7" version = "7.2.7"
description = "Code coverage measurement for Python" description = "Code coverage measurement for Python"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -407,7 +404,6 @@ toml = ["tomli"]
name = "curtsies" name = "curtsies"
version = "0.4.1" version = "0.4.1"
description = "Curses-like terminal wrapper, with colored strings!" description = "Curses-like terminal wrapper, with colored strings!"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -423,7 +419,6 @@ cwcwidth = "*"
name = "cwcwidth" name = "cwcwidth"
version = "0.1.8" version = "0.1.8"
description = "Python bindings for wc(s)width" description = "Python bindings for wc(s)width"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -469,7 +464,6 @@ files = [
name = "distlib" name = "distlib"
version = "0.3.6" version = "0.3.6"
description = "Distribution utilities" description = "Distribution utilities"
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -481,7 +475,6 @@ files = [
name = "docutils" name = "docutils"
version = "0.20.1" version = "0.20.1"
description = "Docutils -- Python Documentation Utilities" description = "Docutils -- Python Documentation Utilities"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -493,7 +486,6 @@ files = [
name = "filelock" name = "filelock"
version = "3.12.0" version = "3.12.0"
description = "A platform independent file lock." description = "A platform independent file lock."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -509,7 +501,6 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "p
name = "greenlet" name = "greenlet"
version = "2.0.2" version = "2.0.2"
description = "Lightweight in-process concurrent programming" description = "Lightweight in-process concurrent programming"
category = "dev"
optional = false optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
files = [ files = [
@ -583,7 +574,6 @@ test = ["objgraph", "psutil"]
name = "grpcio" name = "grpcio"
version = "1.54.2" version = "1.54.2"
description = "HTTP/2-based RPC framework" description = "HTTP/2-based RPC framework"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -641,7 +631,6 @@ protobuf = ["grpcio-tools (>=1.54.2)"]
name = "grpcio-tools" name = "grpcio-tools"
version = "1.54.2" version = "1.54.2"
description = "Protobuf code generator for gRPC" description = "Protobuf code generator for gRPC"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -701,7 +690,6 @@ setuptools = "*"
name = "grpclib" name = "grpclib"
version = "0.4.4" version = "0.4.4"
description = "Pure-Python gRPC implementation for asyncio" description = "Pure-Python gRPC implementation for asyncio"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -719,7 +707,6 @@ protobuf = ["protobuf (>=3.15.0)"]
name = "h2" name = "h2"
version = "4.1.0" version = "4.1.0"
description = "HTTP/2 State-Machine based protocol implementation" description = "HTTP/2 State-Machine based protocol implementation"
category = "main"
optional = false optional = false
python-versions = ">=3.6.1" python-versions = ">=3.6.1"
files = [ files = [
@ -735,7 +722,6 @@ hyperframe = ">=6.0,<7"
name = "hpack" name = "hpack"
version = "4.0.0" version = "4.0.0"
description = "Pure-Python HPACK header compression" description = "Pure-Python HPACK header compression"
category = "main"
optional = false optional = false
python-versions = ">=3.6.1" python-versions = ">=3.6.1"
files = [ files = [
@ -747,7 +733,6 @@ files = [
name = "hyperframe" name = "hyperframe"
version = "6.0.1" version = "6.0.1"
description = "HTTP/2 framing layer for Python" description = "HTTP/2 framing layer for Python"
category = "main"
optional = false optional = false
python-versions = ">=3.6.1" python-versions = ">=3.6.1"
files = [ files = [
@ -759,7 +744,6 @@ files = [
name = "identify" name = "identify"
version = "2.5.24" version = "2.5.24"
description = "File identification library for Python" description = "File identification library for Python"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -774,7 +758,6 @@ license = ["ukkonen"]
name = "idna" name = "idna"
version = "3.4" version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
@ -786,7 +769,6 @@ files = [
name = "imagesize" name = "imagesize"
version = "1.4.1" version = "1.4.1"
description = "Getting image size from png/jpeg/jpeg2000/gif file" description = "Getting image size from png/jpeg/jpeg2000/gif file"
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [ files = [
@ -798,7 +780,6 @@ files = [
name = "importlib-metadata" name = "importlib-metadata"
version = "6.6.0" version = "6.6.0"
description = "Read metadata from Python packages" description = "Read metadata from Python packages"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -819,7 +800,6 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag
name = "iniconfig" name = "iniconfig"
version = "2.0.0" version = "2.0.0"
description = "brain-dead simple config-ini parsing" description = "brain-dead simple config-ini parsing"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -831,7 +811,6 @@ files = [
name = "isort" name = "isort"
version = "5.11.5" version = "5.11.5"
description = "A Python utility / library to sort Python imports." description = "A Python utility / library to sort Python imports."
category = "main"
optional = true optional = true
python-versions = ">=3.7.0" python-versions = ">=3.7.0"
files = [ files = [
@ -849,7 +828,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"]
name = "jinja2" name = "jinja2"
version = "3.1.2" version = "3.1.2"
description = "A very fast and expressive template engine." description = "A very fast and expressive template engine."
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -867,7 +845,6 @@ i18n = ["Babel (>=2.7)"]
name = "jinxed" name = "jinxed"
version = "1.2.0" version = "1.2.0"
description = "Jinxed Terminal Library" description = "Jinxed Terminal Library"
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -882,7 +859,6 @@ ansicon = {version = "*", markers = "platform_system == \"Windows\""}
name = "markupsafe" name = "markupsafe"
version = "2.1.2" version = "2.1.2"
description = "Safely add untrusted strings to HTML/XML markup." description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -942,7 +918,6 @@ files = [
name = "multidict" name = "multidict"
version = "6.0.4" version = "6.0.4"
description = "multidict implementation" description = "multidict implementation"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1026,7 +1001,6 @@ files = [
name = "mypy" name = "mypy"
version = "0.930" version = "0.930"
description = "Optional static typing for Python" description = "Optional static typing for Python"
category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
@ -1066,7 +1040,6 @@ python2 = ["typed-ast (>=1.4.0,<2)"]
name = "mypy-extensions" name = "mypy-extensions"
version = "1.0.0" version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker." description = "Type system extensions for programs checked with the mypy type checker."
category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
@ -1078,7 +1051,6 @@ files = [
name = "nodeenv" name = "nodeenv"
version = "1.8.0" version = "1.8.0"
description = "Node.js virtual environment builder" description = "Node.js virtual environment builder"
category = "dev"
optional = false optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [ files = [
@ -1093,7 +1065,6 @@ setuptools = "*"
name = "packaging" name = "packaging"
version = "23.1" version = "23.1"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1105,7 +1076,6 @@ files = [
name = "pastel" name = "pastel"
version = "0.2.1" version = "0.2.1"
description = "Bring colors to your terminal." description = "Bring colors to your terminal."
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [ files = [
@ -1117,7 +1087,6 @@ files = [
name = "pathspec" name = "pathspec"
version = "0.11.1" version = "0.11.1"
description = "Utility library for gitignore style pattern matching of file paths." description = "Utility library for gitignore style pattern matching of file paths."
category = "main"
optional = true optional = true
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1129,7 +1098,6 @@ files = [
name = "platformdirs" name = "platformdirs"
version = "3.5.1" version = "3.5.1"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1148,7 +1116,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-
name = "pluggy" name = "pluggy"
version = "1.0.0" version = "1.0.0"
description = "plugin and hook calling mechanisms for python" description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
@ -1167,7 +1134,6 @@ testing = ["pytest", "pytest-benchmark"]
name = "poethepoet" name = "poethepoet"
version = "0.19.0" version = "0.19.0"
description = "A task runner that works well with poetry." description = "A task runner that works well with poetry."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1186,7 +1152,6 @@ poetry-plugin = ["poetry (>=1.0,<2.0)"]
name = "pre-commit" name = "pre-commit"
version = "2.21.0" version = "2.21.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks." description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1206,7 +1171,6 @@ virtualenv = ">=20.10.0"
name = "protobuf" name = "protobuf"
version = "4.23.2" version = "4.23.2"
description = "" description = ""
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1229,7 +1193,6 @@ files = [
name = "py" name = "py"
version = "1.11.0" version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities" description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [ files = [
@ -1241,7 +1204,6 @@ files = [
name = "pydantic" name = "pydantic"
version = "1.10.8" version = "1.10.8"
description = "Data validation and settings management using python type hints" description = "Data validation and settings management using python type hints"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1294,7 +1256,6 @@ email = ["email-validator (>=1.0.3)"]
name = "pygments" name = "pygments"
version = "2.15.1" version = "2.15.1"
description = "Pygments is a syntax highlighting package written in Python." description = "Pygments is a syntax highlighting package written in Python."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1309,7 +1270,6 @@ plugins = ["importlib-metadata"]
name = "pytest" name = "pytest"
version = "6.2.5" version = "6.2.5"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
@ -1335,7 +1295,6 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm
name = "pytest-asyncio" name = "pytest-asyncio"
version = "0.12.0" version = "0.12.0"
description = "Pytest support for asyncio." description = "Pytest support for asyncio."
category = "dev"
optional = false optional = false
python-versions = ">= 3.5" python-versions = ">= 3.5"
files = [ files = [
@ -1352,7 +1311,6 @@ testing = ["async_generator (>=1.3)", "coverage", "hypothesis (>=5.7.1)"]
name = "pytest-cov" name = "pytest-cov"
version = "2.12.1" version = "2.12.1"
description = "Pytest plugin for measuring coverage." description = "Pytest plugin for measuring coverage."
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [ files = [
@ -1372,7 +1330,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
name = "pytest-mock" name = "pytest-mock"
version = "3.10.0" version = "3.10.0"
description = "Thin-wrapper around the mock package for easier use with pytest" description = "Thin-wrapper around the mock package for easier use with pytest"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1390,7 +1347,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
name = "python-dateutil" name = "python-dateutil"
version = "2.8.2" version = "2.8.2"
description = "Extensions to the standard Python datetime module" description = "Extensions to the standard Python datetime module"
category = "main"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [ files = [
@ -1405,7 +1361,6 @@ six = ">=1.5"
name = "pytz" name = "pytz"
version = "2023.3" version = "2023.3"
description = "World timezone definitions, modern and historical" description = "World timezone definitions, modern and historical"
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -1417,7 +1372,6 @@ files = [
name = "pyyaml" name = "pyyaml"
version = "6.0" version = "6.0"
description = "YAML parser and emitter for Python" description = "YAML parser and emitter for Python"
category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
@ -1467,7 +1421,6 @@ files = [
name = "requests" name = "requests"
version = "2.31.0" version = "2.31.0"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1489,7 +1442,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
name = "setuptools" name = "setuptools"
version = "67.8.0" version = "67.8.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1506,7 +1458,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
description = "Python 2 and 3 compatibility utilities" description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [ files = [
@ -1518,7 +1469,6 @@ files = [
name = "snowballstemmer" name = "snowballstemmer"
version = "2.2.0" version = "2.2.0"
description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -1530,7 +1480,6 @@ files = [
name = "sphinx" name = "sphinx"
version = "3.1.2" version = "3.1.2"
description = "Python documentation generator" description = "Python documentation generator"
category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
@ -1566,7 +1515,6 @@ test = ["cython", "html5lib", "pytest", "pytest-cov", "typed-ast"]
name = "sphinx-rtd-theme" name = "sphinx-rtd-theme"
version = "0.5.0" version = "0.5.0"
description = "Read the Docs theme for Sphinx" description = "Read the Docs theme for Sphinx"
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -1584,7 +1532,6 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"]
name = "sphinxcontrib-applehelp" name = "sphinxcontrib-applehelp"
version = "1.0.2" version = "1.0.2"
description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
@ -1600,7 +1547,6 @@ test = ["pytest"]
name = "sphinxcontrib-devhelp" name = "sphinxcontrib-devhelp"
version = "1.0.2" version = "1.0.2"
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
@ -1616,7 +1562,6 @@ test = ["pytest"]
name = "sphinxcontrib-htmlhelp" name = "sphinxcontrib-htmlhelp"
version = "2.0.0" version = "2.0.0"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
@ -1632,7 +1577,6 @@ test = ["html5lib", "pytest"]
name = "sphinxcontrib-jsmath" name = "sphinxcontrib-jsmath"
version = "1.0.1" version = "1.0.1"
description = "A sphinx extension which renders display math in HTML via JavaScript" description = "A sphinx extension which renders display math in HTML via JavaScript"
category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
@ -1647,7 +1591,6 @@ test = ["flake8", "mypy", "pytest"]
name = "sphinxcontrib-qthelp" name = "sphinxcontrib-qthelp"
version = "1.0.3" version = "1.0.3"
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
@ -1663,7 +1606,6 @@ test = ["pytest"]
name = "sphinxcontrib-serializinghtml" name = "sphinxcontrib-serializinghtml"
version = "1.1.5" version = "1.1.5"
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
@ -1679,7 +1621,6 @@ test = ["pytest"]
name = "toml" name = "toml"
version = "0.10.2" version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language" description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [ files = [
@ -1691,7 +1632,6 @@ files = [
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1703,7 +1643,6 @@ files = [
name = "tomlkit" name = "tomlkit"
version = "0.7.2" version = "0.7.2"
description = "Style preserving TOML library" description = "Style preserving TOML library"
category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [ files = [
@ -1715,7 +1654,6 @@ files = [
name = "tox" name = "tox"
version = "3.28.0" version = "3.28.0"
description = "tox is a generic virtualenv management and test command line tool" description = "tox is a generic virtualenv management and test command line tool"
category = "dev"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
files = [ files = [
@ -1742,7 +1680,6 @@ testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psu
name = "typed-ast" name = "typed-ast"
version = "1.5.4" version = "1.5.4"
description = "a fork of Python 2 and 3 ast modules with type comment support" description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
@ -1776,7 +1713,6 @@ files = [
name = "typing-extensions" name = "typing-extensions"
version = "4.6.2" version = "4.6.2"
description = "Backported and Experimental Type Hints for Python 3.7+" description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1788,7 +1724,6 @@ files = [
name = "urllib3" name = "urllib3"
version = "2.0.2" version = "2.0.2"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1806,7 +1741,6 @@ zstd = ["zstandard (>=0.18.0)"]
name = "virtualenv" name = "virtualenv"
version = "20.23.0" version = "20.23.0"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1828,7 +1762,6 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess
name = "wcwidth" name = "wcwidth"
version = "0.2.6" version = "0.2.6"
description = "Measures the displayed width of unicode strings in a terminal" description = "Measures the displayed width of unicode strings in a terminal"
category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
@ -1840,7 +1773,6 @@ files = [
name = "zipp" name = "zipp"
version = "3.15.0" version = "3.15.0"
description = "Backport of pathlib-compatible object wrapper for zip files" description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
@ -1858,4 +1790,4 @@ compiler = ["black", "isort", "jinja2"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "8f733a72705d31633a7f198a7a7dd6e3170876a1ccb8ca75b7d94b6379384a8f" content-hash = "f3f3c42f9d20b60b53b7b4639dcd6ccfb945c63a55cb7485de72b5a298d0e618"

View File

@ -19,6 +19,7 @@ importlib-metadata = { version = ">=1.6.0", python = "<3.8" }
jinja2 = { version = ">=3.0.3", optional = true } jinja2 = { version = ">=3.0.3", optional = true }
python-dateutil = "^2.8" python-dateutil = "^2.8"
isort = {version = "^5.11.5", optional = true} isort = {version = "^5.11.5", optional = true}
betterproto-extras = { path = "betterproto-extras" }
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
asv = "^0.4.2" asv = "^0.4.2"

View File

@ -1330,6 +1330,13 @@ class Message(ABC):
:class:`Message` :class:`Message`
The initialized message. The initialized message.
""" """
if True:
# TODO: Make native deserialization optional
import betterproto_extras
betterproto_extras.deserialize(self, data)
return self
with BytesIO(data) as stream: with BytesIO(data) as stream:
return self.load(stream) return self.load(stream)

View File

@ -142,12 +142,12 @@ def monkey_patch_oneof_index():
"betterproto" "betterproto"
], ],
"group", "group",
"oneof_index", "_oneof_index",
) )
object.__setattr__( object.__setattr__(
Field.__dataclass_fields__["oneof_index"].metadata["betterproto"], Field.__dataclass_fields__["oneof_index"].metadata["betterproto"],
"group", "group",
"oneof_index", "_oneof_index",
) )
@ -385,7 +385,7 @@ def is_oneof(proto_field_obj: FieldDescriptorProto) -> bool:
us to tell whether it was set, via the which_one_of interface. us to tell whether it was set, via the which_one_of interface.
""" """
return which_one_of(proto_field_obj, "oneof_index")[0] == "oneof_index" return which_one_of(proto_field_obj, "_oneof_index")[0] == "oneof_index"
@dataclass @dataclass