diff --git a/owl-bot-staging/proto-plus/proto-plus/proto-plus.txt b/owl-bot-staging/proto-plus/proto-plus/proto-plus.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/proto-plus/.coveragerc b/packages/proto-plus/.coveragerc
new file mode 100644
index 000000000000..12a43d1c07bb
--- /dev/null
+++ b/packages/proto-plus/.coveragerc
@@ -0,0 +1,12 @@
+[run]
+branch = True
+
+[report]
+show_missing = True
+omit =
+ proto/marshal/compat.py
+exclude_lines =
+ # Re-enable the standard pragma
+ pragma: NO COVER
+ # Ignore debug-only repr
+ def __repr__
diff --git a/packages/proto-plus/.flake8 b/packages/proto-plus/.flake8
new file mode 100644
index 000000000000..fe63398b556d
--- /dev/null
+++ b/packages/proto-plus/.flake8
@@ -0,0 +1,8 @@
+[flake8]
+ignore =
+ # Closing bracket mismatches opening bracket's line.
+ # This works poorly with type annotations in method declarations.
+ E123, E124
+ # Line over-indented for visual indent.
+ # This works poorly with type annotations in method declarations.
+ E128, E131
diff --git a/packages/proto-plus/.gitignore b/packages/proto-plus/.gitignore
new file mode 100644
index 000000000000..f2185a4d2b5a
--- /dev/null
+++ b/packages/proto-plus/.gitignore
@@ -0,0 +1,61 @@
+*.py[cod]
+*.sw[op]
+
+# C extensions
+*.so
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
+lib
+lib64
+__pycache__
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.nox
+.tox
+.cache
+.pytest_cache
+htmlcov
+
+# Translations
+*.mo
+
+# Mac
+.DS_Store
+
+# Mr Developer
+.mr.developer.cfg
+.project
+.pydevproject
+
+# JetBrains
+.idea
+
+# Built documentation
+docs/_build
+docs/_build_doc2dash
+
+# Virtual environment
+env/
+coverage.xml
+
+# System test environment variables.
+system_tests/local_test_setup
+
+# Make sure a generated file isn't accidentally committed.
+pylintrc
+pylintrc.test
diff --git a/packages/proto-plus/.librarian/state.yaml b/packages/proto-plus/.librarian/state.yaml
new file mode 100644
index 000000000000..7705ed2d30f0
--- /dev/null
+++ b/packages/proto-plus/.librarian/state.yaml
@@ -0,0 +1,10 @@
+image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:8e2c32496077054105bd06c54a59d6a6694287bc053588e24debe6da6920ad91
+libraries:
+ - id: proto-plus
+ version: 1.26.1
+ apis: []
+ source_roots:
+ - .
+ preserve_regex: []
+ remove_regex: []
+ tag_format: v{version}
diff --git a/packages/proto-plus/.readthedocs.yml b/packages/proto-plus/.readthedocs.yml
new file mode 100644
index 000000000000..309b5e76a26c
--- /dev/null
+++ b/packages/proto-plus/.readthedocs.yml
@@ -0,0 +1,6 @@
+---
+build:
+ image: latest
+python:
+ pip_install: true
+ version: 3.9
diff --git a/packages/proto-plus/.repo-metadata.json b/packages/proto-plus/.repo-metadata.json
new file mode 100644
index 000000000000..cbe8a78e01b2
--- /dev/null
+++ b/packages/proto-plus/.repo-metadata.json
@@ -0,0 +1,12 @@
+{
+ "name": "proto-plus",
+ "name_pretty": "Proto Plus",
+ "client_documentation": "https://googleapis.dev/python/proto-plus/latest",
+ "release_level": "stable",
+ "language": "python",
+ "library_type": "CORE",
+ "repo": "googleapis/proto-plus-python",
+ "distribution_name": "proto-plus",
+ "default_version": "",
+ "codeowner_team": ""
+}
diff --git a/packages/proto-plus/CHANGELOG.md b/packages/proto-plus/CHANGELOG.md
new file mode 100644
index 000000000000..0bc4e9acd440
--- /dev/null
+++ b/packages/proto-plus/CHANGELOG.md
@@ -0,0 +1,529 @@
+# Changelog
+
+[PyPI History][1]
+
+[1]: https://pypi.org/project/proto-plus/#history
+
+## [1.26.1](https://github.com/googleapis/proto-plus-python/compare/v1.26.0...v1.26.1) (2025-03-05)
+
+
+### Bug Fixes
+
+* **deps:** Allow protobuf 6.x ([#536](https://github.com/googleapis/proto-plus-python/issues/536)) ([51ba025](https://github.com/googleapis/proto-plus-python/commit/51ba02513c4fa12fe94db74c4a23fed7af972ea9))
+
+## [1.26.0](https://github.com/googleapis/proto-plus-python/compare/v1.25.0...v1.26.0) (2025-01-22)
+
+
+### Features
+
+* Migrate to pyproject.toml ([#496](https://github.com/googleapis/proto-plus-python/issues/496)) ([82ed3b9](https://github.com/googleapis/proto-plus-python/commit/82ed3b91ae0cebd6f89ce6661590a1bc6b7fce31))
+
+
+### Bug Fixes
+
+* Construct messages with nested duration in protobuf 5.28+ ([#519](https://github.com/googleapis/proto-plus-python/issues/519)) ([197ddf8](https://github.com/googleapis/proto-plus-python/commit/197ddf8a3ae9ab21b0136f27692d0f1ecd727d5b))
+* Fix enums initialization in PyPy ([#507](https://github.com/googleapis/proto-plus-python/issues/507)) ([b8b68f2](https://github.com/googleapis/proto-plus-python/commit/b8b68f207a00129e91551ef6725f5021f821e0a9))
+* Incorrect return type annotation for Message.to_dict ([#516](https://github.com/googleapis/proto-plus-python/issues/516)) ([72990f3](https://github.com/googleapis/proto-plus-python/commit/72990f3859d77732d40db5b82c310da265e72cac))
+* Use include rather than exclude to find_namespace_packages in setup.py ([#502](https://github.com/googleapis/proto-plus-python/issues/502)) ([77e252e](https://github.com/googleapis/proto-plus-python/commit/77e252e614f6434c2c47ab6168d08f87e004be43))
+
+
+### Documentation
+
+* Update docs link in README ([#524](https://github.com/googleapis/proto-plus-python/issues/524)) ([a85be75](https://github.com/googleapis/proto-plus-python/commit/a85be75f8fb811f5345cea2786b9b7a688085a7e))
+
+## [1.25.0](https://github.com/googleapis/proto-plus-python/compare/v1.24.0...v1.25.0) (2024-10-15)
+
+
+### Features
+
+* Add support for Python 3.13 ([#493](https://github.com/googleapis/proto-plus-python/issues/493)) ([e9643a1](https://github.com/googleapis/proto-plus-python/commit/e9643a1f6135267d4389c77722120e6c98342a74))
+
+
+### Bug Fixes
+
+* Construct messages with nested struct ([#479](https://github.com/googleapis/proto-plus-python/issues/479)) ([aa4aa61](https://github.com/googleapis/proto-plus-python/commit/aa4aa61b8c7ac0cc34d2d5797999bb434de88737))
+* Fix 'Couldn't build proto file' when using Python 3.13 ([#492](https://github.com/googleapis/proto-plus-python/issues/492)) ([a48c39f](https://github.com/googleapis/proto-plus-python/commit/a48c39ff2212261bc932d10132086a6c55be22e9))
+* Fix conda compatibility issue ([#475](https://github.com/googleapis/proto-plus-python/issues/475)) ([e2f9c9d](https://github.com/googleapis/proto-plus-python/commit/e2f9c9d1c87230d0e8d9ccacdfd0872792d54f1b))
+* Fix issue with equality comparison of repeated field with None ([#477](https://github.com/googleapis/proto-plus-python/issues/477)) ([3476348](https://github.com/googleapis/proto-plus-python/commit/3476348c995af2ce5dfcbcc688e9ddf98fa36360))
+* Remove check for Protobuf version ([#474](https://github.com/googleapis/proto-plus-python/issues/474)) ([a1748a3](https://github.com/googleapis/proto-plus-python/commit/a1748a315b6b50128b0d9927b2fee353ec55975f))
+
+
+### Documentation
+
+* Fix typos in proto/message.py ([#463](https://github.com/googleapis/proto-plus-python/issues/463)) ([4d8ee65](https://github.com/googleapis/proto-plus-python/commit/4d8ee656e008ec2b22f347e5da539b6285ec4b1b))
+* Update message.py spelling error `paylod` → `payload` ([e59fc9a](https://github.com/googleapis/proto-plus-python/commit/e59fc9a4f8dd9fcb0804b77347662ae29d1a31a1))
+
+## [1.24.0](https://github.com/googleapis/proto-plus-python/compare/v1.23.0...v1.24.0) (2024-06-11)
+
+
+### Features
+
+* Add `always_print_fields_with_no_presence` fields to `to_json` and `to_dict` ([0f89372](https://github.com/googleapis/proto-plus-python/commit/0f893724cabe513a5a9f9c8428dbd31f1b4f1d52))
+
+
+### Bug Fixes
+
+* Add compatibility with protobuf==5.x ([0f89372](https://github.com/googleapis/proto-plus-python/commit/0f893724cabe513a5a9f9c8428dbd31f1b4f1d52))
+* AttributeError module 'google._upb._message' has no attribute 'MessageMapContainer' ([0f89372](https://github.com/googleapis/proto-plus-python/commit/0f893724cabe513a5a9f9c8428dbd31f1b4f1d52))
+* **deps:** Allow protobuf 5.x ([#457](https://github.com/googleapis/proto-plus-python/issues/457)) ([62d74e3](https://github.com/googleapis/proto-plus-python/commit/62d74e3275476b82fed48f2119fc761fe2371292))
+* Drop python 3.6 ([#456](https://github.com/googleapis/proto-plus-python/issues/456)) ([5a7666c](https://github.com/googleapis/proto-plus-python/commit/5a7666c15002aee0fab44a9aa6d3279aab3f1f69))
+
+
+### Documentation
+
+* Deprecate field `including_default_value_fields` in `to_json` and `to_dict` ([0f89372](https://github.com/googleapis/proto-plus-python/commit/0f893724cabe513a5a9f9c8428dbd31f1b4f1d52))
+
+## [1.23.0](https://github.com/googleapis/proto-plus-python/compare/v1.22.3...v1.23.0) (2023-12-01)
+
+
+### Features
+
+* Add additional parameters to `to_json()` and `to_dict()` methods ([#384](https://github.com/googleapis/proto-plus-python/issues/384)) ([8f13a46](https://github.com/googleapis/proto-plus-python/commit/8f13a46514e1d7653426c0db3c1021f9c794451a))
+* Add support for proto.__version__ ([#393](https://github.com/googleapis/proto-plus-python/issues/393)) ([48cd63f](https://github.com/googleapis/proto-plus-python/commit/48cd63f2d0a7c62c40d2724f46ac564c9884675b))
+* Add support for python 3.12 ([#400](https://github.com/googleapis/proto-plus-python/issues/400)) ([1b3a96f](https://github.com/googleapis/proto-plus-python/commit/1b3a96fae7a21bf0120a79ba6bf57aacfd2a0db4))
+
+
+### Bug Fixes
+
+* Use setuptools.find_namespace_packages ([#412](https://github.com/googleapis/proto-plus-python/issues/412)) ([30a6864](https://github.com/googleapis/proto-plus-python/commit/30a6864739eb8fb116caa5873044d3999f37f578))
+
+
+### Documentation
+
+* Add documentation on how to query the current oneof in a given message ([#408](https://github.com/googleapis/proto-plus-python/issues/408)) ([d89d811](https://github.com/googleapis/proto-plus-python/commit/d89d81112885f3b3ca4e1342fd2034ee6797fcf6))
+* Add example for __protobuf__ module level attribute ([#409](https://github.com/googleapis/proto-plus-python/issues/409)) ([6755884](https://github.com/googleapis/proto-plus-python/commit/675588450acc4636b2d82b2bc0860a314064c4a4))
+
+## [1.22.3](https://github.com/googleapis/proto-plus-python/compare/v1.22.2...v1.22.3) (2023-06-22)
+
+
+### Bug Fixes
+
+* Resolve issue where marshal fails with cross api dependency ([#348](https://github.com/googleapis/proto-plus-python/issues/348)) ([0dcea18](https://github.com/googleapis/proto-plus-python/commit/0dcea18898cdc2170a945f3d96216bae6a37e60f))
+
+## [1.22.2](https://github.com/googleapis/proto-plus-python/compare/v1.22.1...v1.22.2) (2023-01-05)
+
+
+### Bug Fixes
+
+* Add support for Python 3.11 ([#329](https://github.com/googleapis/proto-plus-python/issues/329)) ([5cff3a0](https://github.com/googleapis/proto-plus-python/commit/5cff3a0d703fc90f757f1d150adc0dfd62aa3d2e))
+
+
+### Documentation
+
+* Fix typo in index.rst ([#342](https://github.com/googleapis/proto-plus-python/issues/342)) ([a66a378](https://github.com/googleapis/proto-plus-python/commit/a66a3782802a1088c775a6b29adb15fa0235e1f1))
+
+## [1.22.1](https://github.com/googleapis/proto-plus-python/compare/v1.22.0...v1.22.1) (2022-08-29)
+
+
+### Bug Fixes
+
+* Add no-pretty print option ([#336](https://github.com/googleapis/proto-plus-python/issues/336)) ([1bb228a](https://github.com/googleapis/proto-plus-python/commit/1bb228ac93543d28871645a22e5ac7fb20a0a55c))
+
+## [1.22.0](https://github.com/googleapis/proto-plus-python/compare/v1.21.0...v1.22.0) (2022-08-10)
+
+### Features
+
+* Add support for protobuf v4 ([#327](https://github.com/googleapis/proto-plus-python/issues/327)) ([ed353aa](https://github.com/googleapis/proto-plus-python/commit/ed353aaf8bd5a659535eb493221320e449f3f637))
+
+### Bug Fixes
+
+* Fix Timestamp, Duration and FieldMask marshaling in REST transport ([a2e7300](https://github.com/googleapis/proto-plus-python/commit/a2e7300368625ceec39dd2c2dfb96291ad8cf1f1))
+* fixes bug in the test. ([#332](https://github.com/googleapis/proto-plus-python/issues/332)) ([f85f470](https://github.com/googleapis/proto-plus-python/commit/f85f470c880a7bff7f3c813d33d15e657e7b5123))
+
+## [1.20.6](https://github.com/googleapis/proto-plus-python/compare/v1.20.5...v1.20.6) (2022-06-13)
+
+
+### Bug Fixes
+
+* **deps:** allow protobuf < 5.0.0 ([#324](https://github.com/googleapis/proto-plus-python/issues/324)) ([af4f56e](https://github.com/googleapis/proto-plus-python/commit/af4f56edb0bfbfa808f7def37e19b24bd27d4b40))
+
+
+### Documentation
+
+* fix changelog header to consistent size ([#319](https://github.com/googleapis/proto-plus-python/issues/319)) ([27d2003](https://github.com/googleapis/proto-plus-python/commit/27d2003571342e24aa74d29a45fa49d2328ff76d))
+
+## [1.20.5](https://github.com/googleapis/proto-plus-python/compare/v1.20.4...v1.20.5) (2022-05-26)
+
+
+### Bug Fixes
+
+* **deps:** require google-api-core[grpc] >= 1.31.5 ([1d13c41](https://github.com/googleapis/proto-plus-python/commit/1d13c415df457a87153a6fca202003fa83e56093))
+* **deps:** require protobuf>= 3.15.0, <4.0.0dev ([#315](https://github.com/googleapis/proto-plus-python/issues/315)) ([1d13c41](https://github.com/googleapis/proto-plus-python/commit/1d13c415df457a87153a6fca202003fa83e56093))
+
+## [1.20.4](https://github.com/googleapis/proto-plus-python/compare/v1.20.3...v1.20.4) (2022-05-02)
+
+
+### Bug Fixes
+
+* default proto package name is the module name, not "" ([#309](https://github.com/googleapis/proto-plus-python/issues/309)) ([3148a1c](https://github.com/googleapis/proto-plus-python/commit/3148a1c287eb69b397c940119cd44e5067357e17))
+* lookup attribute instead of performing a deepcopy ([#226](https://github.com/googleapis/proto-plus-python/issues/226)) ([e469059](https://github.com/googleapis/proto-plus-python/commit/e469059d70bab4c2b0a38dd52c4451b3c61bf470))
+
+## [1.20.3](https://github.com/googleapis/proto-plus-python/compare/v1.20.2...v1.20.3) (2022-02-18)
+
+
+### Bug Fixes
+
+* additional logic to mitigate collisions with reserved terms ([#301](https://github.com/googleapis/proto-plus-python/issues/301)) ([c9a77df](https://github.com/googleapis/proto-plus-python/commit/c9a77df58e93a87952470d809538a08103644364))
+
+## [1.20.2](https://github.com/googleapis/proto-plus-python/compare/v1.20.1...v1.20.2) (2022-02-17)
+
+
+### Bug Fixes
+
+* dir(proto.Message) does not raise ([#302](https://github.com/googleapis/proto-plus-python/issues/302)) ([80dcce9](https://github.com/googleapis/proto-plus-python/commit/80dcce9099e630a6217792b6b3a14213add690e6))
+
+## [1.20.1](https://github.com/googleapis/proto-plus-python/compare/v1.20.0...v1.20.1) (2022-02-14)
+
+
+### Bug Fixes
+
+* mitigate collisions in field names ([#295](https://github.com/googleapis/proto-plus-python/issues/295)) ([158ae99](https://github.com/googleapis/proto-plus-python/commit/158ae995aa4fdf6239c864a41f5df5575a3c30b3))
+
+## [1.20.0](https://github.com/googleapis/proto-plus-python/compare/v1.19.9...v1.20.0) (2022-02-07)
+
+
+### Features
+
+* add custom __dir__ for messages and message classes ([#289](https://github.com/googleapis/proto-plus-python/issues/289)) ([35e019e](https://github.com/googleapis/proto-plus-python/commit/35e019eb8155c1e4067b326804e3e7e86f85b6a8))
+
+
+### Bug Fixes
+
+* workaround for buggy pytest ([#291](https://github.com/googleapis/proto-plus-python/issues/291)) ([28aa3b2](https://github.com/googleapis/proto-plus-python/commit/28aa3b2b325d2ba262f35cfc8d20e1f5fbdcf883))
+
+## [1.19.9](https://github.com/googleapis/proto-plus-python/compare/v1.19.8...v1.19.9) (2022-01-25)
+
+
+### Bug Fixes
+
+* add pickling support to proto messages ([#280](https://github.com/googleapis/proto-plus-python/issues/280)) ([2b7be35](https://github.com/googleapis/proto-plus-python/commit/2b7be3563f9fc2a4649a5e14d7653b85020c566f))
+
+## [1.19.8](https://www.github.com/googleapis/proto-plus-python/compare/v1.19.7...v1.19.8) (2021-11-09)
+
+
+### Documentation
+
+* fix typos ([#277](https://www.github.com/googleapis/proto-plus-python/issues/277)) ([e3b71e8](https://www.github.com/googleapis/proto-plus-python/commit/e3b71e8b2a81a5abb5af666c9625facb1814a609))
+
+## [1.19.7](https://www.github.com/googleapis/proto-plus-python/compare/v1.19.6...v1.19.7) (2021-10-27)
+
+
+### Bug Fixes
+
+* restore allowing None as value for stringy ints ([#272](https://www.github.com/googleapis/proto-plus-python/issues/272)) ([a8991d7](https://www.github.com/googleapis/proto-plus-python/commit/a8991d71ff455093fbfef142f9140d3f2928195f))
+
+## [1.19.6](https://www.github.com/googleapis/proto-plus-python/compare/v1.19.5...v1.19.6) (2021-10-25)
+
+
+### Bug Fixes
+
+* setting 64bit fields from strings supported ([#267](https://www.github.com/googleapis/proto-plus-python/issues/267)) ([ea7b911](https://www.github.com/googleapis/proto-plus-python/commit/ea7b91100114f5c3d40d41320b045568ac9a68f9))
+
+## [1.19.5](https://www.github.com/googleapis/proto-plus-python/compare/v1.19.4...v1.19.5) (2021-10-11)
+
+
+### Documentation
+
+* Clarify semantics of multiple oneof variants passed to msg ctor ([#263](https://www.github.com/googleapis/proto-plus-python/issues/263)) ([6f8a5b2](https://www.github.com/googleapis/proto-plus-python/commit/6f8a5b2098e4f6748945c53bda3d5821e62e5a0a))
+
+## [1.19.4](https://www.github.com/googleapis/proto-plus-python/compare/v1.19.3...v1.19.4) (2021-10-08)
+
+
+### Documentation
+
+* clarify that proto plus messages are not pickleable ([#260](https://www.github.com/googleapis/proto-plus-python/issues/260)) ([6e691dc](https://www.github.com/googleapis/proto-plus-python/commit/6e691dc27b1e540ef0661597fd89ece8f0155c97))
+
+## [1.19.3](https://www.github.com/googleapis/proto-plus-python/compare/v1.19.2...v1.19.3) (2021-10-07)
+
+
+### Bug Fixes
+
+* setting bytes field from python string base64 decodes before assignment ([#255](https://www.github.com/googleapis/proto-plus-python/issues/255)) ([b6f3eb6](https://www.github.com/googleapis/proto-plus-python/commit/b6f3eb6575484748126170997b8c98512763ea66))
+
+## [1.19.2](https://www.github.com/googleapis/proto-plus-python/compare/v1.19.1...v1.19.2) (2021-09-29)
+
+
+### Bug Fixes
+
+* ensure enums are hashable ([#252](https://www.github.com/googleapis/proto-plus-python/issues/252)) ([232341b](https://www.github.com/googleapis/proto-plus-python/commit/232341b4f4902fba1b3597bb1e1618b8f320374b))
+
+## [1.19.1](https://www.github.com/googleapis/proto-plus-python/compare/v1.19.0...v1.19.1) (2021-09-29)
+
+
+### Bug Fixes
+
+* ensure enums are incomparable w other enum types ([#248](https://www.github.com/googleapis/proto-plus-python/issues/248)) ([5927c14](https://www.github.com/googleapis/proto-plus-python/commit/5927c1400f400b3213c9b92e7a37c3c3a1abd681))
+
+## [1.19.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.18.1...v1.19.0) (2021-06-29)
+
+
+### Features
+
+* pass 'including_default_value_fields' through to 'Message.to_dict' method ([#232](https://www.github.com/googleapis/proto-plus-python/issues/232)) ([15c2f47](https://www.github.com/googleapis/proto-plus-python/commit/15c2f479f81f0f80d451ca9b043e42cecfe7184e))
+
+## [1.18.1](https://www.github.com/googleapis/proto-plus-python/compare/v1.18.0...v1.18.1) (2021-03-19)
+
+
+### Bug Fixes
+
+* Add arm64 support for PY3.6 ([#219](https://www.github.com/googleapis/proto-plus-python/issues/219)) ([c9667c2](https://www.github.com/googleapis/proto-plus-python/commit/c9667c22d0b8f6026dbf69d502eb8eb972279891))
+
+## [1.18.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.17.0...v1.18.0) (2021-03-16)
+
+
+### Features
+
+* add copy_from method for field assignment ([#215](https://www.github.com/googleapis/proto-plus-python/issues/215)) ([11c3e58](https://www.github.com/googleapis/proto-plus-python/commit/11c3e58a9ba59f0d7d808a26597dab735ca982ba))
+
+## [1.17.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.16.0...v1.17.0) (2021-03-12)
+
+
+### Features
+
+* add preserving_proto_field_name to to_json ([#213](https://www.github.com/googleapis/proto-plus-python/issues/213)) ([b2c245b](https://www.github.com/googleapis/proto-plus-python/commit/b2c245bf044b964897f4e7423ff4944ae915e469))
+
+## [1.16.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.15.0...v1.16.0) (2021-03-12)
+
+
+### Features
+
+* add preserving_proto_field_name passthrough in MessageMeta.to_dict ([#211](https://www.github.com/googleapis/proto-plus-python/issues/211)) ([7675a0c](https://www.github.com/googleapis/proto-plus-python/commit/7675a0c8d1004f2727d64100527f2b208d305017))
+
+## [1.15.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.14.3...v1.15.0) (2021-03-10)
+
+
+### Features
+
+* allow_alias for enums ([#207](https://www.github.com/googleapis/proto-plus-python/issues/207)) ([6d4d713](https://www.github.com/googleapis/proto-plus-python/commit/6d4d71399f494b9f3bd47b6f3ef0b6d3c0c547b5))
+
+## [1.14.3](https://www.github.com/googleapis/proto-plus-python/compare/v1.14.2...v1.14.3) (2021-03-04)
+
+
+### Bug Fixes
+
+* adding enums to a repeated field does not raise a TypeError ([#202](https://www.github.com/googleapis/proto-plus-python/issues/202)) ([2a10bbe](https://www.github.com/googleapis/proto-plus-python/commit/2a10bbecaf8955c7bf1956086aef42630112788b))
+
+## [1.14.2](https://www.github.com/googleapis/proto-plus-python/compare/v1.14.1...v1.14.2) (2021-02-26)
+
+
+### Bug Fixes
+
+* use the correct environment for uploading to pypi ([#199](https://www.github.com/googleapis/proto-plus-python/issues/199)) ([babdc5c](https://www.github.com/googleapis/proto-plus-python/commit/babdc5cddf08235cac3cda66200babab44204688))
+
+## [1.14.1](https://www.github.com/googleapis/proto-plus-python/compare/v1.14.0...v1.14.1) (2021-02-26)
+
+
+### Bug Fixes
+
+* install the wheel dependency ([#197](https://www.github.com/googleapis/proto-plus-python/issues/197)) ([923ab31](https://www.github.com/googleapis/proto-plus-python/commit/923ab31e4685b47acae793198be55335e5eeae38))
+
+## [1.14.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.13.1...v1.14.0) (2021-02-24)
+
+
+### Features
+
+* Pypi publish ghub actions ([#189](https://www.github.com/googleapis/proto-plus-python/issues/189)) ([4c967b0](https://www.github.com/googleapis/proto-plus-python/commit/4c967b0bb2ead29156bcc53c1f3b227b3afb2e8b))
+
+
+### Bug Fixes
+
+* proper __setitem__ and insert for RepeatedComposite ([#178](https://www.github.com/googleapis/proto-plus-python/issues/178)) ([1157a76](https://www.github.com/googleapis/proto-plus-python/commit/1157a76bb608d72389f46dc4d8e9aa00cc14ccc6))
+* proper native marshal for repeated enumeration fields ([#180](https://www.github.com/googleapis/proto-plus-python/issues/180)) ([30265d6](https://www.github.com/googleapis/proto-plus-python/commit/30265d654d7f3589cbd0994d2ac564db1fd44265))
+
+## [1.13.1](https://www.github.com/googleapis/proto-plus-python/compare/v1.13.0...v1.13.1) (2021-02-09)
+
+
+### Bug Fixes
+
+* update docstring to match type hint ([#172](https://www.github.com/googleapis/proto-plus-python/issues/172)) ([14dad5b](https://www.github.com/googleapis/proto-plus-python/commit/14dad5bf6c5967a720e9d3095d621dbfe208b614))
+
+## [1.13.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.12.0...v1.13.0) (2020-12-04)
+
+
+### Features
+
+* add 3.9 support and drop 3.5 ([#167](https://www.github.com/googleapis/proto-plus-python/issues/167)) ([6d17195](https://www.github.com/googleapis/proto-plus-python/commit/6d171956e14b398aece931b9dde1013be9644b74))
+
+## [1.12.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.11.0...v1.12.0) (2020-11-20)
+
+
+### Features
+
+* add default values parameter to to_json ([#164](https://www.github.com/googleapis/proto-plus-python/issues/164)) ([691f1b2](https://www.github.com/googleapis/proto-plus-python/commit/691f1b24454502c4ac49a88a09d1c9fbc287b2bd))
+
+## [1.11.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.10.2...v1.11.0) (2020-10-19)
+
+
+### Features
+
+* provide a to_dict method ([#154](https://www.github.com/googleapis/proto-plus-python/issues/154)) ([ccf903e](https://www.github.com/googleapis/proto-plus-python/commit/ccf903e3cddfcb1ff539e853594b4342914b7d61)), closes [#153](https://www.github.com/googleapis/proto-plus-python/issues/153) [#151](https://www.github.com/googleapis/proto-plus-python/issues/151)
+
+## [1.10.2](https://www.github.com/googleapis/proto-plus-python/compare/v1.10.1...v1.10.2) (2020-10-14)
+
+
+### Documentation
+
+* explain how to use repeated struct.Value ([#148](https://www.github.com/googleapis/proto-plus-python/issues/148)) ([9634ea8](https://www.github.com/googleapis/proto-plus-python/commit/9634ea8fa464c0d34f13469016f23cc2e986d973)), closes [#104](https://www.github.com/googleapis/proto-plus-python/issues/104)
+
+## [1.10.1](https://www.github.com/googleapis/proto-plus-python/compare/v1.10.0...v1.10.1) (2020-10-08)
+
+
+### Bug Fixes
+
+* accessing an unset struct_pb2.Value field does not raise ([#140](https://www.github.com/googleapis/proto-plus-python/issues/140)) ([d045cbf](https://www.github.com/googleapis/proto-plus-python/commit/d045cbf058cbb8f4ca98dd06741270fcaee865be))
+* add LICENSE and tests to package ([#146](https://www.github.com/googleapis/proto-plus-python/issues/146)) ([815c943](https://www.github.com/googleapis/proto-plus-python/commit/815c9439a1dadb2d4111784eb18ba673ce6e6cc2))
+
+## [1.10.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.9.1...v1.10.0) (2020-09-24)
+
+
+### Bug Fixes
+
+* loosen tag match for publish_package ([#123](https://www.github.com/googleapis/proto-plus-python/issues/123)) ([67441c9](https://www.github.com/googleapis/proto-plus-python/commit/67441c931b5f00b2e1084ce2539784ae9d9c31e6))
+* third party enums don't break first class enums ([#118](https://www.github.com/googleapis/proto-plus-python/issues/118)) ([50b87af](https://www.github.com/googleapis/proto-plus-python/commit/50b87af481bb1f19d10d64e88dc9ee39c2d5b6f8)), closes [#103](https://www.github.com/googleapis/proto-plus-python/issues/103)
+
+
+## [1.10.0-dev2](https://www.github.com/googleapis/proto-plus-python/compare/v1.9.1...v1.10.0-dev2) (2020-09-21)
+
+
+### Bug Fixes
+
+* loosen tag match for publish_package ([#123](https://www.github.com/googleapis/proto-plus-python/issues/123)) ([67441c9](https://www.github.com/googleapis/proto-plus-python/commit/67441c931b5f00b2e1084ce2539784ae9d9c31e6))
+* third party enums don't break first class enums ([#118](https://www.github.com/googleapis/proto-plus-python/issues/118)) ([50b87af](https://www.github.com/googleapis/proto-plus-python/commit/50b87af481bb1f19d10d64e88dc9ee39c2d5b6f8)), closes [#103](https://www.github.com/googleapis/proto-plus-python/issues/103)
+
+
+## [1.9.1](https://www.github.com/googleapis/proto-plus-python/compare/v1.9.0...v1.9.1) (2020-09-08)
+
+
+### Reverts
+
+* Revert "feat: json serialization and deserialization support stringy enums (#112)" (#116) ([91c6d7b](https://www.github.com/googleapis/proto-plus-python/commit/91c6d7bb27d198439bb323d2454fb94e197bf3dd)), closes [#112](https://www.github.com/googleapis/proto-plus-python/issues/112) [#116](https://www.github.com/googleapis/proto-plus-python/issues/116)
+
+
+### Documentation
+
+* update README ([#120](https://www.github.com/googleapis/proto-plus-python/issues/120)) ([2077390](https://www.github.com/googleapis/proto-plus-python/commit/2077390d614acb278ab94077f131a895d7184881))
+
+## [1.9.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.8.1...v1.9.0) (2020-09-02)
+
+
+### Features
+
+* json serialization and deserialization support stringy enums ([#112](https://www.github.com/googleapis/proto-plus-python/issues/112)) ([8d2e3a3](https://www.github.com/googleapis/proto-plus-python/commit/8d2e3a3439650dab9ca7c6ff49ed067838a02a45)), closes [#107](https://www.github.com/googleapis/proto-plus-python/issues/107)
+
+## [1.8.1](https://www.github.com/googleapis/proto-plus-python/compare/v1.8.0...v1.8.1) (2020-08-28)
+
+
+### Bug Fixes
+
+* revert "feat: allow enum strings in json serialization and deserialization" ([#110](https://www.github.com/googleapis/proto-plus-python/issues/110)) ([bd3d50e](https://www.github.com/googleapis/proto-plus-python/commit/bd3d50e6b4d4574a21592f51adf7b248ededd545)), closes [#107](https://www.github.com/googleapis/proto-plus-python/issues/107)
+
+## [1.8.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.7.1...v1.8.0) (2020-08-28)
+
+
+### Features
+
+* allow enum strings in json serialization and deserialization ([#107](https://www.github.com/googleapis/proto-plus-python/issues/107)) ([a082f85](https://www.github.com/googleapis/proto-plus-python/commit/a082f85ffcb72e2c53c0e33e40e6df2927a41259))
+
+## [1.7.1](https://www.github.com/googleapis/proto-plus-python/compare/v1.7.0...v1.7.1) (2020-08-17)
+
+
+### Bug Fixes
+
+* revert algorithm for RepeatedComposite insertion. ([#101](https://www.github.com/googleapis/proto-plus-python/issues/101)) ([ae946aa](https://www.github.com/googleapis/proto-plus-python/commit/ae946aa2a3b394fa31590224fcf50593bde0ccaa))
+
+## [1.7.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.6.0...v1.7.0) (2020-08-07)
+
+
+### Features
+
+* optimize insert for class RepeatedComposite. ([#95](https://www.github.com/googleapis/proto-plus-python/issues/95)) ([86790e3](https://www.github.com/googleapis/proto-plus-python/commit/86790e3f7d891e13835699a4e1f50aec6140fa6e))
+
+## [1.6.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.5.3...v1.6.0) (2020-08-05)
+
+
+### Features
+
+* more performance optimizations ([#92](https://www.github.com/googleapis/proto-plus-python/issues/92)) ([19b1519](https://www.github.com/googleapis/proto-plus-python/commit/19b151960de7c83ac82e670b06cb47d6e885f627))
+
+## [1.5.3](https://www.github.com/googleapis/proto-plus-python/compare/v1.5.2...v1.5.3) (2020-08-04)
+
+
+### Bug Fixes
+
+* yet more perf tweaks ([#90](https://www.github.com/googleapis/proto-plus-python/issues/90)) ([eb7891c](https://www.github.com/googleapis/proto-plus-python/commit/eb7891cf05124803352b2f4fd719937356bf9167))
+
+## [1.5.2](https://www.github.com/googleapis/proto-plus-python/compare/v1.5.1...v1.5.2) (2020-08-03)
+
+
+### Bug Fixes
+
+* tweak to_python ([#88](https://www.github.com/googleapis/proto-plus-python/issues/88)) ([5459ede](https://www.github.com/googleapis/proto-plus-python/commit/5459ede75597b06df5a211b0e317fb2c1f4b034e))
+
+## [1.5.1](https://www.github.com/googleapis/proto-plus-python/compare/v1.5.0...v1.5.1) (2020-07-30)
+
+
+### Bug Fixes
+
+* numerous small performance tweaks ([#85](https://www.github.com/googleapis/proto-plus-python/issues/85)) ([7b5faf2](https://www.github.com/googleapis/proto-plus-python/commit/7b5faf2e2c20c8022c83d6a99656505aa669200b))
+
+## [1.5.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.4.2...v1.5.0) (2020-07-29)
+
+
+### Features
+
+* support fixed filename salt to allow proto-plus use with schema registry tools ([#61](https://www.github.com/googleapis/proto-plus-python/issues/61)) ([ea86eb9](https://www.github.com/googleapis/proto-plus-python/commit/ea86eb9ac694ed1f0e711698429449f41ecfedfc))
+
+## [1.4.2](https://www.github.com/googleapis/proto-plus-python/compare/v1.4.1...v1.4.2) (2020-07-23)
+
+
+### Bug Fixes
+
+* getattr on an invalid field raises AttributeError ([#73](https://www.github.com/googleapis/proto-plus-python/issues/73)) ([74ea8f0](https://www.github.com/googleapis/proto-plus-python/commit/74ea8f0cd9083939e53d1de2450b649500281b9a)), closes [#31](https://www.github.com/googleapis/proto-plus-python/issues/31)
+
+## [1.4.1](https://www.github.com/googleapis/proto-plus-python/compare/v1.4.0...v1.4.1) (2020-07-23)
+
+
+### Bug Fixes
+
+* tweak publish ci task ([#65](https://www.github.com/googleapis/proto-plus-python/issues/65)) ([983189c](https://www.github.com/googleapis/proto-plus-python/commit/983189c5effa25fb9365eb63caddb425d3cfb2b5))
+
+## [1.4.0](https://www.github.com/googleapis/proto-plus-python/compare/v1.3.2...v1.4.0) (2020-07-23)
+
+
+### Features
+
+* prevent unnecessary copies when deserializing proto ([#63](https://www.github.com/googleapis/proto-plus-python/issues/63)) ([5e1c061](https://www.github.com/googleapis/proto-plus-python/commit/5e1c0619b5f4c5d2a6a75ae6d45a53fef2e58823))
+
+## [1.3.2](https://www.github.com/googleapis/proto-plus-python/compare/v1.3.1...v1.3.2) (2020-07-22)
+
+
+### Bug Fixes
+
+* correctly handle passed in vanilla datetime.datetime ([#57](https://www.github.com/googleapis/proto-plus-python/issues/57)) ([a770816](https://www.github.com/googleapis/proto-plus-python/commit/a770816197cbce60ee023bd5b6ee6bd2d970ded8)), closes [googleapis/gapic-generator-python#544](https://www.github.com/googleapis/gapic-generator-python/issues/544)
+* update encrypted pypi passwd ([#58](https://www.github.com/googleapis/proto-plus-python/issues/58)) ([d985233](https://www.github.com/googleapis/proto-plus-python/commit/d9852336d83717cb9ff24b6bec3ef1463239fea1))
+
+## [1.3.1](https://www.github.com/googleapis/proto-plus-python/compare/v1.3.0...v1.3.1) (2020-07-21)
+
+
+### Bug Fixes
+
+* tweak pypi circleci task ([#54](https://www.github.com/googleapis/proto-plus-python/issues/54)) ([89c49d7](https://www.github.com/googleapis/proto-plus-python/commit/89c49d700d4b6e9a434fbfced8ca39d430dd22f9))
+
+
+### Documentation
+
+* linkify pypi badge ([#50](https://www.github.com/googleapis/proto-plus-python/issues/50)) ([8ff08e2](https://www.github.com/googleapis/proto-plus-python/commit/8ff08e21e75570aad71c5e62f4c78b43139b5df2))
+
+## [1.3.0](https://www.github.com/googleapis/proto-plus-python/compare/1.2.0...v1.3.0) (2020-07-16)
+
+
+### Features
+
+* add convenience methods to convert to/from json ([#39](https://www.github.com/googleapis/proto-plus-python/issues/39)) ([2868946](https://www.github.com/googleapis/proto-plus-python/commit/286894609843f568c9ff367ab79542783642b801))
+* add DatetimeWithNanoseconds class to maintain Timestamp pb precision. ([#40](https://www.github.com/googleapis/proto-plus-python/issues/40)) ([a17ccd5](https://www.github.com/googleapis/proto-plus-python/commit/a17ccd52c7fa3609ce79fde84b931c0693f53171)), closes [#38](https://www.github.com/googleapis/proto-plus-python/issues/38)
+* add support for proto3 optional fields ([#35](https://www.github.com/googleapis/proto-plus-python/issues/35)) ([0eb5762](https://www.github.com/googleapis/proto-plus-python/commit/0eb5762681e315635db1dffd583d91a4f32cba43))
+
+
+### Bug Fixes
+
+* Modify setup.py to indicate this is google maintained ([#45](https://www.github.com/googleapis/proto-plus-python/issues/45)) ([96b3b00](https://www.github.com/googleapis/proto-plus-python/commit/96b3b00dd6712fe44e71dedf8080b20544e95416))
diff --git a/packages/proto-plus/CODE_OF_CONDUCT.md b/packages/proto-plus/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000000..46b2a08ea6d1
--- /dev/null
+++ b/packages/proto-plus/CODE_OF_CONDUCT.md
@@ -0,0 +1,43 @@
+# Contributor Code of Conduct
+
+As contributors and maintainers of this project,
+and in the interest of fostering an open and welcoming community,
+we pledge to respect all people who contribute through reporting issues,
+posting feature requests, updating documentation,
+submitting pull requests or patches, and other activities.
+
+We are committed to making participation in this project
+a harassment-free experience for everyone,
+regardless of level of experience, gender, gender identity and expression,
+sexual orientation, disability, personal appearance,
+body size, race, ethnicity, age, religion, or nationality.
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery
+* Personal attacks
+* Trolling or insulting/derogatory comments
+* Public or private harassment
+* Publishing other's private information,
+such as physical or electronic
+addresses, without explicit permission
+* Other unethical or unprofessional conduct.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct.
+By adopting this Code of Conduct,
+project maintainers commit themselves to fairly and consistently
+applying these principles to every aspect of managing this project.
+Project maintainers who do not follow or enforce the Code of Conduct
+may be permanently removed from the project team.
+
+This code of conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior
+may be reported by opening an issue
+or contacting one or more of the project maintainers.
+
+This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
+available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
diff --git a/packages/proto-plus/CONTRIBUTING.md b/packages/proto-plus/CONTRIBUTING.md
new file mode 100644
index 000000000000..6272489dae31
--- /dev/null
+++ b/packages/proto-plus/CONTRIBUTING.md
@@ -0,0 +1,28 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution;
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code Reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google/conduct/).
diff --git a/packages/proto-plus/CONTRIBUTING.rst b/packages/proto-plus/CONTRIBUTING.rst
new file mode 100644
index 000000000000..2e0d8efd6790
--- /dev/null
+++ b/packages/proto-plus/CONTRIBUTING.rst
@@ -0,0 +1,22 @@
+Contributing
+============
+
+We are thrilled that you are interested in contributing to this project.
+Please open an issue or pull request with your ideas.
+
+
+Contributor License Agreements
+------------------------------
+
+Before we can accept your pull requests, you will need to sign a Contributor
+License Agreement (CLA):
+
+- **If you are an individual writing original source code** and **you own the
+ intellectual property**, then you'll need to sign an
+ `individual CLA `__.
+- **If you work for a company that wants to allow you to contribute your work**,
+ then you'll need to sign a
+ `corporate CLA `__.
+
+You can sign these electronically (just scroll to the bottom). After that,
+we'll be able to accept your pull requests.
diff --git a/packages/proto-plus/LICENSE b/packages/proto-plus/LICENSE
new file mode 100644
index 000000000000..d64569567334
--- /dev/null
+++ b/packages/proto-plus/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/proto-plus/MANIFEST.in b/packages/proto-plus/MANIFEST.in
new file mode 100644
index 000000000000..e6c966606293
--- /dev/null
+++ b/packages/proto-plus/MANIFEST.in
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+include README.rst LICENSE
+recursive-include tests *
+global-exclude *.py[co]
+global-exclude __pycache__
\ No newline at end of file
diff --git a/packages/proto-plus/README.rst b/packages/proto-plus/README.rst
new file mode 100644
index 000000000000..0fcef70f8734
--- /dev/null
+++ b/packages/proto-plus/README.rst
@@ -0,0 +1,26 @@
+Proto Plus for Python
+=====================
+
+|pypi| |release level|
+
+ Beautiful, Pythonic protocol buffers.
+
+This is a wrapper around `protocol buffers`_. Protocol buffers is a
+specification format for APIs, such as those inside Google.
+This library provides protocol buffer message classes and objects that
+largely behave like native Python types.
+
+.. _protocol buffers: https://developers.google.com/protocol-buffers/
+
+
+Documentation
+-------------
+
+`API Documentation`_ is available on Read the Docs.
+
+.. _API Documentation: https://googleapis.dev/python/proto-plus/latest/
+
+.. |pypi| image:: https://img.shields.io/pypi/v/proto-plus.svg
+ :target: https://pypi.org/project/proto-plus
+.. |release level| image:: https://img.shields.io/badge/release%20level-ga-gold.svg?style=flat
+ :target: https://cloud.google.com/terms/launch-stages
diff --git a/packages/proto-plus/SECURITY.md b/packages/proto-plus/SECURITY.md
new file mode 100644
index 000000000000..8b58ae9c01ae
--- /dev/null
+++ b/packages/proto-plus/SECURITY.md
@@ -0,0 +1,7 @@
+# Security Policy
+
+To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
+
+The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
+
+We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.
diff --git a/packages/proto-plus/docs/Makefile b/packages/proto-plus/docs/Makefile
new file mode 100644
index 000000000000..f9f9a54ca7dc
--- /dev/null
+++ b/packages/proto-plus/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+SPHINXPROJ = APIClientGeneratorforPython
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/packages/proto-plus/docs/conf.py b/packages/proto-plus/docs/conf.py
new file mode 100644
index 000000000000..ecc295d25a44
--- /dev/null
+++ b/packages/proto-plus/docs/conf.py
@@ -0,0 +1,182 @@
+# -*- coding: utf-8 -*-
+#
+# Configuration file for the Sphinx documentation builder.
+#
+# This file does only contain a selection of the most common options. For a
+# full list see the documentation:
+# http://www.sphinx-doc.org/en/stable/config
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+
+import proto
+
+
+sys.path.insert(0, os.path.abspath(".."))
+
+
+# -- Project information -----------------------------------------------------
+
+project = "Proto Plus for Python"
+copyright = "2018, Google LLC"
+author = "Google LLC"
+
+version = proto.version.__version__
+
+
+# -- General configuration ---------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.githubpages",
+ "sphinx.ext.napoleon",
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = ".rst"
+
+# The master toctree document.
+master_doc = "index"
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = "en"
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path .
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = "sphinx"
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = "sphinx_rtd_theme"
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = []
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# The default sidebars (for documents that don't match any pattern) are
+# defined by theme itself. Builtin themes are using these templates by
+# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
+# 'searchbox.html']``.
+#
+# html_sidebars = {}
+
+
+# -- Options for HTMLHelp output ---------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "proto-plus-python"
+
+
+# -- Options for LaTeX output ------------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+ # Latex figure (float) alignmentAPIClientGeneratorforPython
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (
+ master_doc,
+ "proto-plus-python.tex",
+ "Proto Plus for Python Documentation",
+ "Luke Sneeringer",
+ "manual",
+ ),
+]
+
+
+# -- Options for manual page output ------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (
+ master_doc,
+ "proto-plus-python",
+ "Proto Plus for Python Documentation",
+ [author],
+ 1,
+ )
+]
+
+
+# -- Options for Texinfo output ----------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (
+ master_doc,
+ "proto-plus-python",
+ "Proto Plus for Python Documentation",
+ author,
+ "proto-plus-python",
+ "One line description of project.",
+ "Miscellaneous",
+ ),
+]
+
+
+# -- Extension configuration -------------------------------------------------
+
+# -- Options for intersphinx extension ---------------------------------------
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
diff --git a/packages/proto-plus/docs/fields.rst b/packages/proto-plus/docs/fields.rst
new file mode 100644
index 000000000000..1961cff07407
--- /dev/null
+++ b/packages/proto-plus/docs/fields.rst
@@ -0,0 +1,246 @@
+Fields
+======
+
+Fields are assigned using the :class:`~.Field` class, instantiated within a
+:class:`~.Message` declaration.
+
+Fields always have a type (either a primitive, a message, or an enum) and a
+``number``.
+
+.. code-block:: python
+
+ import proto
+
+ class Composer(proto.Message):
+ given_name = proto.Field(proto.STRING, number=1)
+ family_name = proto.Field(proto.STRING, number=2)
+
+ class Song(proto.Message):
+ composer = proto.Field(Composer, number=1)
+ title = proto.Field(proto.STRING, number=2)
+ lyrics = proto.Field(proto.STRING, number=3)
+ year = proto.Field(proto.INT32, number=4)
+
+
+
+For messages and enums, assign the message or enum class directly (as shown
+in the example above).
+
+.. note::
+
+ For messages declared in the same module, it is also possible to use a
+ string with the message class' name *if* the class is not
+ yet declared, which allows for declaring messages out of order or with
+ circular references.
+
+Repeated fields
+---------------
+
+Some fields are actually repeated fields. In protocol buffers, repeated fields
+are generally equivalent to typed lists. In protocol buffers, these are
+declared using the **repeated** keyword:
+
+.. code-block:: protobuf
+
+ message Album {
+ repeated Song songs = 1;
+ string publisher = 2;
+ }
+
+Declare them in Python using the :class:`~.RepeatedField` class:
+
+.. code-block:: python
+
+ class Album(proto.Message):
+ songs = proto.RepeatedField(Song, number=1)
+ publisher = proto.Field(proto.STRING, number=2)
+
+
+.. note::
+
+ Elements **must** be appended individually for repeated fields of `struct.Value`.
+
+ .. code-block:: python
+
+ class Row(proto.Message):
+ values = proto.RepeatedField(proto.MESSAGE, number=1, message=struct.Value,)
+
+ >>> row = Row()
+ >>> values = [struct_pb2.Value(string_value="hello")]
+ >>> for v in values:
+ >>> row.values.append(v)
+
+ Direct assignment will result in an error.
+
+ .. code-block:: python
+
+ class Row(proto.Message):
+ values = proto.RepeatedField(proto.MESSAGE, number=1, message=struct.Value,)
+
+ >>> row = Row()
+ >>> row.values = [struct_pb2.Value(string_value="hello")]
+ Traceback (most recent call last):
+ File "", line 1, in
+ File "/usr/local/google/home/busunkim/github/python-automl/.nox/unit-3-8/lib/python3.8/site-packages/proto/message.py", line 543, in __setattr__
+ self._pb.MergeFrom(self._meta.pb(**{key: pb_value}))
+ TypeError: Value must be iterable
+
+
+Map fields
+----------
+
+Similarly, some fields are map fields. In protocol buffers, map fields are
+equivalent to typed dictionaries, where the keys are either strings or
+integers, and the values can be any type. In protocol buffers, these use
+a special ``map`` syntax:
+
+.. code-block:: protobuf
+
+ message Album {
+ map track_list = 1;
+ string publisher = 2;
+ }
+
+Declare them in Python using the :class:`~.MapField` class:
+
+.. code-block:: python
+
+ class Album(proto.Message):
+ track_list = proto.MapField(proto.UINT32, Song, number=1)
+ publisher = proto.Field(proto.STRING, number=2)
+
+
+Oneofs (mutually-exclusive fields)
+----------------------------------
+
+Protocol buffers allows certain fields to be declared as mutually exclusive.
+This is done by wrapping fields in a ``oneof`` syntax:
+
+.. code-block:: protobuf
+
+ import "google/type/postal_address.proto";
+
+ message AlbumPurchase {
+ Album album = 1;
+ oneof delivery {
+ google.type.PostalAddress postal_address = 2;
+ string download_uri = 3;
+ }
+ }
+
+When using this syntax, protocol buffers will enforce that only one of the
+given fields is set on the message, and setting a field within the oneof
+will clear any others.
+
+Declare this in Python using the ``oneof`` keyword-argument, which takes
+a string (which should match for all fields within the oneof):
+
+.. code-block:: python
+
+ from google.type.postal_address import PostalAddress
+
+ class AlbumPurchase(proto.Message):
+ album = proto.Field(Album, number=1)
+ postal_address = proto.Field(PostalAddress, number=2, oneof='delivery')
+ download_uri = proto.Field(proto.STRING, number=3, oneof='delivery')
+
+.. warning::
+
+ ``oneof`` fields **must** be declared consecutively, otherwise the C
+ implementation of protocol buffers will reject the message. They need not
+ have consecutive field numbers, but they must be declared in consecutive
+ order.
+
+.. warning::
+
+ If a message is constructed with multiple variants of a single ``oneof`` passed
+ to its constructor, the **last** keyword/value pair passed will be the final
+ value set.
+
+ This is consistent with PEP-468_, which specifies the order that keyword args
+ are seen by called functions, and with the regular protocol buffers runtime,
+ which exhibits the same behavior.
+
+ Example:
+
+ .. code-block:: python
+
+ import proto
+
+ class Song(proto.Message):
+ name = proto.Field(proto.STRING, number=1, oneof="identifier")
+ database_id = proto.Field(proto.STRING, number=2, oneof="identifier")
+
+ s = Song(name="Canon in D minor", database_id="b5a37aad3")
+ assert "database_id" in s and "name" not in s
+
+ s = Song(database_id="e6aa708c7e", name="Little Fugue")
+ assert "name" in s and "database_id" not in s
+
+ To query which ``oneof`` is present in a given message, use ``proto.Message._pb("oneof")``.
+
+ Example:
+
+ .. code-block:: python
+
+ import proto
+
+ class Song(proto.Message):
+ name = proto.Field(proto.STRING, number=1, oneof="identifier")
+ database_id = proto.Field(proto.STRING, number=2, oneof="identifier")
+
+ s = Song(name="Canon in D minor")
+ assert s._pb.WhichOneof("identifier") == "name"
+
+ s = Song(database_id="e6aa708c7e")
+ assert s._pb.WhichOneof("identifier") == "database_id"
+
+
+Optional fields
+---------------
+
+All fields in protocol buffers are optional, but it is often necessary to
+check for field presence. Sometimes legitimate values for fields can be falsy,
+so checking for truthiness is not sufficient. Proto3 v3.12.0 added the
+``optional`` keyword to field descriptions, which enables a mechanism for
+checking field presence.
+
+In proto plus, fields can be marked as optional by passing ``optional=True``
+in the constructor. The message *class* then gains a field of the same name
+that can be used to detect whether the field is present in message *instances*.
+
+.. code-block:: python
+
+ class Song(proto.Message):
+ composer = proto.Field(Composer, number=1)
+ title = proto.Field(proto.STRING, number=2)
+ lyrics = proto.Field(proto.STRING, number=3)
+ year = proto.Field(proto.INT32, number=4)
+ performer = proto.Field(proto.STRING, number=5, optional=True)
+
+ >>> s = Song(
+ ... composer={'given_name': 'Johann', 'family_name': 'Pachelbel'},
+ ... title='Canon in D',
+ ... year=1680,
+ ... genre=Genre.CLASSICAL,
+ ... )
+ >>> Song.performer in s
+ False
+ >>> s.performer = 'Brahms'
+ >>> Song.performer in s
+ True
+ >>> del s.performer
+ >>> Song.performer in s
+ False
+ >>> s.performer = "" # The mysterious, unnamed composer
+ >>> Song.performer in s
+ True
+
+
+Under the hood, fields marked as optional are implemented via a synthetic
+one-variant ``oneof``. See the protocolbuffers documentation_ for more
+information.
+
+.. _documentation: https://github.com/protocolbuffers/protobuf/blob/v3.12.0/docs/field_presence.md
+
+.. _PEP-468: https://www.python.org/dev/peps/pep-0468/
diff --git a/packages/proto-plus/docs/index.rst b/packages/proto-plus/docs/index.rst
new file mode 100644
index 000000000000..d94608dd5c97
--- /dev/null
+++ b/packages/proto-plus/docs/index.rst
@@ -0,0 +1,39 @@
+Proto Plus for Python
+=====================
+
+ Beautiful, Pythonic protocol buffers.
+
+This library provides a clean, readable, straightforward pattern for
+declaring messages in `protocol buffers`_. It provides a wrapper around
+the official implementation, so that using messages feels natural while
+retaining the power and flexibility of protocol buffers.
+
+.. _protocol buffers: https://developers.google.com/protocol-buffers/
+
+
+Installing
+----------
+
+Install this library using ``pip``:
+
+.. code-block:: shell
+
+ $ pip install proto-plus
+
+This library carries a dependency on the official implementation
+(protobuf_), which may install a C component.
+
+.. _protobuf: https://pypi.org/project/protobuf/
+
+
+Table of Contents
+-----------------
+
+.. toctree::
+ :maxdepth: 2
+
+ messages
+ fields
+ marshal
+ status
+ reference/index
diff --git a/packages/proto-plus/docs/marshal.rst b/packages/proto-plus/docs/marshal.rst
new file mode 100644
index 000000000000..ee61cb2e6846
--- /dev/null
+++ b/packages/proto-plus/docs/marshal.rst
@@ -0,0 +1,84 @@
+Type Marshaling
+===============
+
+Proto Plus provides a service that converts between protocol buffer objects
+and native Python types (or the wrapper types provided by this library).
+
+This allows native Python objects to be used in place of protocol buffer
+messages where appropriate. In all cases, we return the native type, and are
+liberal on what we accept.
+
+Well-known types
+----------------
+
+The following types are currently handled by Proto Plus:
+
+=================================== ======================= ========
+Protocol buffer type Python type Nullable
+=================================== ======================= ========
+``google.protobuf.BoolValue`` ``bool`` Yes
+``google.protobuf.BytesValue`` ``bytes`` Yes
+``google.protobuf.DoubleValue`` ``float`` Yes
+``google.protobuf.Duration`` ``datetime.timedelta`` –
+``google.protobuf.FloatValue`` ``float`` Yes
+``google.protobuf.Int32Value`` ``int`` Yes
+``google.protobuf.Int64Value`` ``int`` Yes
+``google.protobuf.ListValue`` ``MutableSequence`` Yes
+``google.protobuf.StringValue`` ``str`` Yes
+``google.protobuf.Struct`` ``MutableMapping`` Yes
+``google.protobuf.Timestamp`` ``datetime.datetime`` Yes
+``google.protobuf.UInt32Value`` ``int`` Yes
+``google.protobuf.UInt64Value`` ``int`` Yes
+``google.protobuf.Value`` JSON-encodable values Yes
+=================================== ======================= ========
+
+.. note::
+
+ Protocol buffers include well-known types for ``Timestamp`` and
+ ``Duration``, both of which have nanosecond precision. However, the
+ Python ``datetime`` and ``timedelta`` objects have only microsecond
+ precision. This library converts timestamps to an implementation of
+ ``datetime.datetime``, DatetimeWithNanoseconds, that includes nanosecond
+ precision.
+
+ If you *write* a timestamp field using a Python ``datetime`` value,
+ any existing nanosecond precision will be overwritten.
+
+.. note::
+
+ Setting a ``bytes`` field from a string value will first base64 decode the string.
+ This is necessary to preserve the original protobuf semantics when converting between
+ Python dicts and proto messages.
+ Converting a message containing a bytes field to a dict will
+ base64 encode the bytes field and yield a value of type str.
+
+.. code-block:: python
+
+ import proto
+ from google.protobuf.json_format import ParseDict
+
+ class MyMessage(proto.Message):
+ data = proto.Field(proto.BYTES, number=1)
+
+ msg = MyMessage(data=b"this is a message")
+ msg_dict = MyMessage.to_dict(msg)
+
+ # Note: the value is the base64 encoded string of the bytes field.
+ # It has a type of str, NOT bytes.
+ assert type(msg_dict['data']) == str
+
+ msg_pb = ParseDict(msg_dict, MyMessage.pb())
+ msg_two = MyMessage(msg_dict)
+
+ assert msg == msg_pb == msg_two
+
+
+Wrapper types
+-------------
+
+Additionally, every :class:`~.Message` subclass is a wrapper class. The
+creation of the class also creates the underlying protocol buffer class, and
+this is registered to the marshal.
+
+The underlying protocol buffer message class is accessible with the
+:meth:`~.Message.pb` class method.
diff --git a/packages/proto-plus/docs/messages.rst b/packages/proto-plus/docs/messages.rst
new file mode 100644
index 000000000000..68d7d63d4d7f
--- /dev/null
+++ b/packages/proto-plus/docs/messages.rst
@@ -0,0 +1,304 @@
+Messages
+========
+
+The fundamental building block in protocol buffers are `messages`_.
+Messages are essentially permissive, strongly-typed structs (dictionaries),
+which have zero or more fields that may themselves contain primitives or
+other messages.
+
+.. code-block:: protobuf
+
+ syntax = "proto3";
+
+ message Song {
+ Composer composer = 1;
+ string title = 2;
+ string lyrics = 3;
+ int32 year = 4;
+ }
+
+ message Composer {
+ string given_name = 1;
+ string family_name = 2;
+ }
+
+The most common use case for protocol buffers is to write a ``.proto`` file,
+and then use the protocol buffer compiler to generate code for it.
+
+
+Declaring messages
+------------------
+
+However, it is possible to declare messages directly.
+This is the equivalent message declaration in Python, using this library:
+
+.. code-block:: python
+
+ import proto
+
+ class Composer(proto.Message):
+ given_name = proto.Field(proto.STRING, number=1)
+ family_name = proto.Field(proto.STRING, number=2)
+
+ class Song(proto.Message):
+ composer = proto.Field(Composer, number=1)
+ title = proto.Field(proto.STRING, number=2)
+ lyrics = proto.Field(proto.STRING, number=3)
+ year = proto.Field(proto.INT32, number=4)
+
+A few things to note:
+
+* This library only handles proto3.
+* The ``number`` is really a field ID. It is *not* a value of any kind.
+* All fields are optional (as is always the case in proto3).
+ The only general way to determine whether a field was explicitly set to its
+ falsy value or not set all is to mark it ``optional``.
+* Because all fields are optional, it is the responsibility of application logic
+ to determine whether a necessary field has been set.
+* You can optionally define a `__protobuf__` attribute in your module which will be used
+ to differentiate messages which have the same name but exist in different modules.
+
+.. code-block:: python
+
+ # file a.py
+ import proto
+
+ __protobuf__ = proto.module(package="a")
+
+ class A(proto.Message):
+ name = proto.Field(proto.STRING, number=1)
+
+ # file b.py
+ import proto
+
+ __protobuf__ = proto.module(package="b")
+
+ class A(proto.Message):
+ name = proto.Field(proto.STRING, number=1)
+
+ # file main.py
+ import a
+ import b
+
+ _a = a.A(name="Hello, A!")
+ _b = b.A(name="Hello, B!")
+
+.. _messages: https://developers.google.com/protocol-buffers/docs/proto3#simple
+
+Messages are fundamentally made up of :doc:`fields`. Most messages are nothing
+more than a name and their set of fields.
+
+
+Usage
+-----
+
+Instantiate messages using either keyword arguments or a :class:`dict`
+(and mix and matching is acceptable):
+
+.. code-block:: python
+
+ >>> song = Song(
+ ... composer={'given_name': 'Johann', 'family_name': 'Pachelbel'},
+ ... title='Canon in D',
+ ... year=1680,
+ ... )
+ >>> song.composer.family_name
+ 'Pachelbel'
+ >>> song.title
+ 'Canon in D'
+
+
+Assigning to Fields
+-------------------
+
+One of the goals of proto-plus is to make protobufs feel as much like regular python
+objects as possible. It is possible to update a message's field by assigning to it,
+just as if it were a regular python object.
+
+.. code-block:: python
+
+ song = Song()
+ song.composer = Composer(given_name="Johann", family_name="Bach")
+
+ # Can also assign from a dictionary as a convenience.
+ song.composer = {"given_name": "Claude", "family_name": "Debussy"}
+
+ # Repeated fields can also be assigned
+ class Album(proto.Message):
+ songs = proto.RepeatedField(Song, number=1)
+
+ a = Album()
+ songs = [Song(title="Canon in D"), Song(title="Little Fugue")]
+ a.songs = songs
+
+.. note::
+
+ Assigning to a proto-plus message field works by making copies, not by updating references.
+ This is necessary because of memory layout requirements of protocol buffers.
+ These memory constraints are maintained by the protocol buffers runtime.
+ This behavior can be surprising under certain circumstances, e.g. trying to save
+ an alias to a nested field.
+
+ :class:`proto.Message` defines a helper message, :meth:`~.Message.copy_from` to
+ help make the distinction clear when reading code.
+ The semantics of :meth:`~.Message.copy_from` are identical to the field assignment behavior described above.
+
+ .. code-block:: python
+
+ composer = Composer(given_name="Johann", family_name="Bach")
+ song = Song(title="Tocatta and Fugue in D Minor", composer=composer)
+ composer.given_name = "Wilhelm"
+
+ # 'composer' is NOT a reference to song.composer
+ assert song.composer.given_name == "Johann"
+
+ # We CAN update the song's composer by assignment.
+ song.composer = composer
+ composer.given_name = "Carl"
+
+ # 'composer' is STILL not a reference to song.composer.
+ assert song.composer.given_name == "Wilhelm"
+
+ # It does work in reverse, though,
+ # if we want a reference we can access then update.
+ composer = song.composer
+ composer.given_name = "Gottfried"
+
+ assert song.composer.given_name == "Gottfried"
+
+ # We can use 'copy_from' if we're concerned that the code
+ # implies that assignment involves references.
+ composer = Composer(given_name="Elisabeth", family_name="Bach")
+ # We could also do Message.copy_from(song.composer, composer) instead.
+ Composer.copy_from(song.composer, composer)
+
+ assert song.composer.given_name == "Elisabeth"
+
+
+Enums
+-----
+
+Enums are also supported:
+
+.. code-block:: python
+
+ import proto
+
+ class Genre(proto.Enum):
+ GENRE_UNSPECIFIED = 0
+ CLASSICAL = 1
+ JAZZ = 2
+ ROCK = 3
+
+ class Composer(proto.Message):
+ given_name = proto.Field(proto.STRING, number=1)
+ family_name = proto.Field(proto.STRING, number=2)
+
+ class Song(proto.Message):
+ composer = proto.Field(Composer, number=1)
+ title = proto.Field(proto.STRING, number=2)
+ lyrics = proto.Field(proto.STRING, number=3)
+ year = proto.Field(proto.INT32, number=4)
+ genre = proto.Field(Genre, number=5)
+
+All enums **must** begin with a ``0`` value, which is always the default in
+proto3 (and, as above, indistuiguishable from unset).
+
+Enums utilize Python :class:`enum.IntEnum` under the hood:
+
+.. code-block:: python
+
+ >>> song = Song(
+ ... composer={'given_name': 'Johann', 'family_name': 'Pachelbel'},
+ ... title='Canon in D',
+ ... year=1680,
+ ... genre=Genre.CLASSICAL,
+ ... )
+ >>> song.genre
+
+ >>> song.genre.name
+ 'CLASSICAL'
+ >>> song.genre.value
+ 1
+
+Additionally, it is possible to provide strings or plain integers:
+
+.. code-block:: python
+
+ >>> song.genre = 2
+ >>> song.genre
+
+ >>> song.genre = 'CLASSICAL'
+
+
+Serialization
+-------------
+
+Serialization and deserialization is available through the
+:meth:`~.Message.serialize` and :meth:`~.Message.deserialize` class methods.
+
+The :meth:`~.Message.serialize` method is available on the message *classes*
+only, and accepts an instance:
+
+.. code-block:: python
+
+ serialized_song = Song.serialize(song)
+
+The :meth:`~.Message.deserialize` method accepts a :class:`bytes`, and
+returns an instance of the message:
+
+.. code-block:: python
+
+ song = Song.deserialize(serialized_song)
+
+JSON serialization and deserialization are also available from message *classes*
+via the :meth:`~.Message.to_json` and :meth:`~.Message.from_json` methods.
+
+.. code-block:: python
+
+ json = Song.to_json(song)
+
+ new_song = Song.from_json(json)
+
+Similarly, messages can be converted into dictionaries via the
+:meth:`~.Message.to_dict` helper method.
+There is no :meth:`~.Message.from_dict` method because the Message constructor
+already allows construction from mapping types.
+
+.. code-block:: python
+
+ song_dict = Song.to_dict(song)
+
+ new_song = Song(song_dict)
+
+.. note::
+
+ Although Python's pickling protocol has known issues when used with
+ untrusted collaborators, some frameworks do use it for communication
+ between trusted hosts. To support such frameworks, protobuf messages
+ **can** be pickled and unpickled, although the preferred mechanism for
+ serializing proto messages is :meth:`~.Message.serialize`.
+
+ Multiprocessing example:
+
+ .. code-block:: python
+
+ import proto
+ from multiprocessing import Pool
+
+ class Composer(proto.Message):
+ name = proto.Field(proto.STRING, number=1)
+ genre = proto.Field(proto.STRING, number=2)
+
+ composers = [Composer(name=n) for n in ["Bach", "Mozart", "Brahms", "Strauss"]]
+
+ with multiprocessing.Pool(2) as p:
+ def add_genre(comp_bytes):
+ composer = Composer.deserialize(comp_bytes)
+ composer.genre = "classical"
+ return Composer.serialize(composer)
+
+ updated_composers = [
+ Composer.deserialize(comp_bytes)
+ for comp_bytes in p.map(add_genre, (Composer.serialize(comp) for comp in composers))
+ ]
diff --git a/packages/proto-plus/docs/reference/datetime_helpers.rst b/packages/proto-plus/docs/reference/datetime_helpers.rst
new file mode 100644
index 000000000000..e45ad1fe4c39
--- /dev/null
+++ b/packages/proto-plus/docs/reference/datetime_helpers.rst
@@ -0,0 +1,5 @@
+Datetime Helpers
+----------------
+
+.. automodule:: proto.datetime_helpers
+ :members:
diff --git a/packages/proto-plus/docs/reference/index.rst b/packages/proto-plus/docs/reference/index.rst
new file mode 100644
index 000000000000..35b01946e51f
--- /dev/null
+++ b/packages/proto-plus/docs/reference/index.rst
@@ -0,0 +1,19 @@
+Reference
+---------
+
+Below is a reference for the major classes and functions within this
+module.
+
+- The :doc:`message` section (which uses the ``message`` and ``fields``
+ modules) handles constructing messages.
+- The :doc:`marshal` module handles translating between internal protocol
+ buffer instances and idiomatic equivalents.
+- The :doc:`datetime_helpers` has datetime related helpers to maintain
+ nanosecond precision.
+
+.. toctree::
+ :maxdepth: 2
+
+ message
+ marshal
+ datetime_helpers
diff --git a/packages/proto-plus/docs/reference/marshal.rst b/packages/proto-plus/docs/reference/marshal.rst
new file mode 100644
index 000000000000..73caec4d4c0e
--- /dev/null
+++ b/packages/proto-plus/docs/reference/marshal.rst
@@ -0,0 +1,5 @@
+Marshal
+-------
+
+.. automodule:: proto.marshal
+ :members:
diff --git a/packages/proto-plus/docs/reference/message.rst b/packages/proto-plus/docs/reference/message.rst
new file mode 100644
index 000000000000..436b0c819176
--- /dev/null
+++ b/packages/proto-plus/docs/reference/message.rst
@@ -0,0 +1,21 @@
+Message and Field
+-----------------
+
+.. autoclass:: proto.message.Message
+ :members:
+
+ .. automethod:: pb
+ .. automethod:: wrap
+ .. automethod:: serialize
+ .. automethod:: deserialize
+ .. automethod:: to_json
+ .. automethod:: from_json
+ .. automethod:: to_dict
+ .. automethod:: copy_from
+
+.. automodule:: proto.fields
+ :members:
+
+
+.. automodule:: proto.enums
+ :members:
diff --git a/packages/proto-plus/docs/status.rst b/packages/proto-plus/docs/status.rst
new file mode 100644
index 000000000000..6004b127ed76
--- /dev/null
+++ b/packages/proto-plus/docs/status.rst
@@ -0,0 +1,17 @@
+Status
+======
+
+Features and Limitations
+------------------------
+
+Nice things this library does:
+
+- Idiomatic protocol buffer message representation and usage.
+- Wraps the official protocol buffers implementation, and exposes its objects
+ in the public API so that they are available where needed.
+
+
+Upcoming work
+-------------
+
+- Specialized behavior for ``google.protobuf.FieldMask`` objects.
diff --git a/packages/proto-plus/noxfile.py b/packages/proto-plus/noxfile.py
new file mode 100644
index 000000000000..1e43d8264ac8
--- /dev/null
+++ b/packages/proto-plus/noxfile.py
@@ -0,0 +1,172 @@
+# Copyright 2017, Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+
+import nox
+import pathlib
+
+
+CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute()
+
+
+PYTHON_VERSIONS = [
+ "3.7",
+ "3.8",
+ "3.9",
+ "3.10",
+ "pypy3.10",
+ "3.11",
+ "3.12",
+ "3.13",
+ "3.14",
+]
+
+# Error if a python version is missing
+nox.options.error_on_missing_interpreters = True
+
+
+@nox.session(python=PYTHON_VERSIONS)
+@nox.parametrize("implementation", ["cpp", "upb", "python"])
+def unit(session, implementation):
+ """Run the unit test suite."""
+
+ constraints_path = str(
+ CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
+ )
+
+ session.env["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = implementation
+ session.install("coverage", "pytest", "pytest-cov", "pytz")
+ session.install("-e", ".[testing]", "-c", constraints_path)
+ # TODO(https://github.com/googleapis/proto-plus-python/issues/389):
+ # Remove the 'cpp' implementation once support for Protobuf 3.x is dropped.
+ # The 'cpp' implementation requires Protobuf<4.
+ if implementation == "cpp":
+ session.install("protobuf<4")
+
+ # TODO(https://github.com/googleapis/proto-plus-python/issues/403): re-enable `-W=error`
+ # The warnings-as-errors flag `-W=error` was removed in
+ # https://github.com/googleapis/proto-plus-python/pull/400.
+ # It should be re-added once issue
+ # https://github.com/protocolbuffers/protobuf/issues/15077 is fixed.
+ session.run(
+ "pytest",
+ "--quiet",
+ *(
+ session.posargs # Coverage info when running individual tests is annoying.
+ or [
+ "--cov=proto",
+ "--cov-config=.coveragerc",
+ "--cov-report=term",
+ "--cov-report=html",
+ "tests",
+ ]
+ ),
+ )
+
+
+# Only test upb and python implementation backends.
+# As of protobuf 4.x, the "ccp" implementation is not available in the PyPI package as per
+# https://github.com/protocolbuffers/protobuf/tree/main/python#implementation-backends
+@nox.session(python=PYTHON_VERSIONS[-1])
+@nox.parametrize("implementation", ["python", "upb"])
+def prerelease_deps(session, implementation):
+ """Run the unit test suite against pre-release versions of dependencies."""
+
+ session.env["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = implementation
+
+ # Install test environment dependencies
+ session.install("coverage", "pytest", "pytest-cov", "pytz")
+
+ # Install the package without dependencies
+ session.install("-e", ".", "--no-deps")
+
+ prerel_deps = [
+ "google-api-core",
+ # dependency of google-api-core
+ "googleapis-common-protos",
+ ]
+
+ for dep in prerel_deps:
+ session.install("--pre", "--no-deps", "--upgrade", dep)
+
+ session.install("--pre", "--upgrade", "protobuf")
+ # Print out prerelease package versions
+ session.run(
+ "python", "-c", "import google.protobuf; print(google.protobuf.__version__)"
+ )
+ session.run(
+ "python", "-c", "import google.api_core; print(google.api_core.__version__)"
+ )
+
+ # TODO(https://github.com/googleapis/proto-plus-python/issues/403): re-enable `-W=error`
+ # The warnings-as-errors flag `-W=error` was removed in
+ # https://github.com/googleapis/proto-plus-python/pull/400.
+ # It should be re-added once issue
+ # https://github.com/protocolbuffers/protobuf/issues/15077 is fixed.
+ session.run(
+ "pytest",
+ "--quiet",
+ *(
+ session.posargs # Coverage info when running individual tests is annoying.
+ or [
+ "--cov=proto",
+ "--cov-config=.coveragerc",
+ "--cov-report=term",
+ "--cov-report=html",
+ "tests",
+ ]
+ ),
+ )
+
+
+@nox.session(python="3.10")
+def docs(session):
+ """Build the docs."""
+
+ session.install(
+ # We need to pin to specific versions of the `sphinxcontrib-*` packages
+ # which still support sphinx 4.x.
+ # See https://github.com/googleapis/sphinx-docfx-yaml/issues/344
+ # and https://github.com/googleapis/sphinx-docfx-yaml/issues/345.
+ "sphinxcontrib-applehelp==1.0.4",
+ "sphinxcontrib-devhelp==1.0.2",
+ "sphinxcontrib-htmlhelp==2.0.1",
+ "sphinxcontrib-qthelp==1.0.3",
+ "sphinxcontrib-serializinghtml==1.1.5",
+ "sphinx==4.5.0",
+ "sphinx_rtd_theme",
+ )
+ session.install(".")
+
+ # Build the docs!
+ session.run("rm", "-rf", "docs/_build/")
+ session.run(
+ "sphinx-build",
+ "-W",
+ "-b",
+ "html",
+ "-d",
+ "docs/_build/doctrees",
+ "docs/",
+ "docs/_build/html/",
+ )
+
+
+@nox.session(python="3.10")
+def lint_setup_py(session):
+ """Verify that setup.py is valid (including RST check)."""
+
+ session.install("docutils", "Pygments")
+ session.run("python", "setup.py", "check", "--restructuredtext", "--strict")
diff --git a/packages/proto-plus/proto/__init__.py b/packages/proto-plus/proto/__init__.py
new file mode 100644
index 000000000000..8780991c0426
--- /dev/null
+++ b/packages/proto-plus/proto/__init__.py
@@ -0,0 +1,72 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .enums import Enum
+from .fields import Field
+from .fields import MapField
+from .fields import RepeatedField
+from .marshal import Marshal
+from .message import Message
+from .modules import define_module as module
+from .primitives import ProtoType
+from .version import __version__
+
+
+DOUBLE = ProtoType.DOUBLE
+FLOAT = ProtoType.FLOAT
+INT64 = ProtoType.INT64
+UINT64 = ProtoType.UINT64
+INT32 = ProtoType.INT32
+FIXED64 = ProtoType.FIXED64
+FIXED32 = ProtoType.FIXED32
+BOOL = ProtoType.BOOL
+STRING = ProtoType.STRING
+MESSAGE = ProtoType.MESSAGE
+BYTES = ProtoType.BYTES
+UINT32 = ProtoType.UINT32
+ENUM = ProtoType.ENUM
+SFIXED32 = ProtoType.SFIXED32
+SFIXED64 = ProtoType.SFIXED64
+SINT32 = ProtoType.SINT32
+SINT64 = ProtoType.SINT64
+
+
+__all__ = (
+ "__version__",
+ "Enum",
+ "Field",
+ "MapField",
+ "RepeatedField",
+ "Marshal",
+ "Message",
+ "module",
+ # Expose the types directly.
+ "DOUBLE",
+ "FLOAT",
+ "INT64",
+ "UINT64",
+ "INT32",
+ "FIXED64",
+ "FIXED32",
+ "BOOL",
+ "STRING",
+ "MESSAGE",
+ "BYTES",
+ "UINT32",
+ "ENUM",
+ "SFIXED32",
+ "SFIXED64",
+ "SINT32",
+ "SINT64",
+)
diff --git a/packages/proto-plus/proto/_file_info.py b/packages/proto-plus/proto/_file_info.py
new file mode 100644
index 000000000000..537eeaf4556a
--- /dev/null
+++ b/packages/proto-plus/proto/_file_info.py
@@ -0,0 +1,196 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import collections
+import inspect
+import logging
+
+from google.protobuf import descriptor_pb2
+from google.protobuf import descriptor_pool
+from google.protobuf import message
+from google.protobuf import reflection
+
+from proto.marshal.rules.message import MessageRule
+
+log = logging.getLogger("_FileInfo")
+
+
+class _FileInfo(
+ collections.namedtuple(
+ "_FileInfo",
+ ["descriptor", "messages", "enums", "name", "nested", "nested_enum"],
+ )
+):
+ registry = {} # Mapping[str, '_FileInfo']
+
+ @classmethod
+ def maybe_add_descriptor(cls, filename, package):
+ descriptor = cls.registry.get(filename)
+ if not descriptor:
+ descriptor = cls.registry[filename] = cls(
+ descriptor=descriptor_pb2.FileDescriptorProto(
+ name=filename,
+ package=package,
+ syntax="proto3",
+ ),
+ enums=collections.OrderedDict(),
+ messages=collections.OrderedDict(),
+ name=filename,
+ nested={},
+ nested_enum={},
+ )
+
+ return descriptor
+
+ @staticmethod
+ def proto_file_name(name):
+ return "{0}.proto".format(name.replace(".", "/"))
+
+ def _get_manifest(self, new_class):
+ module = inspect.getmodule(new_class)
+ if hasattr(module, "__protobuf__"):
+ return frozenset(module.__protobuf__.manifest)
+
+ return frozenset()
+
+ def _get_remaining_manifest(self, new_class):
+ return self._get_manifest(new_class) - {new_class.__name__}
+
+ def _calculate_salt(self, new_class, fallback):
+ manifest = self._get_manifest(new_class)
+ if manifest and new_class.__name__ not in manifest:
+ log.warning(
+ "proto-plus module {module} has a declared manifest but {class_name} is not in it".format(
+ module=inspect.getmodule(new_class).__name__,
+ class_name=new_class.__name__,
+ )
+ )
+
+ return "" if new_class.__name__ in manifest else (fallback or "").lower()
+
+ def generate_file_pb(self, new_class, fallback_salt=""):
+ """Generate the descriptors for all protos in the file.
+
+ This method takes the file descriptor attached to the parent
+ message and generates the immutable descriptors for all of the
+ messages in the file descriptor. (This must be done in one fell
+ swoop for immutability and to resolve proto cross-referencing.)
+
+ This is run automatically when the last proto in the file is
+ generated, as determined by the module's __all__ tuple.
+ """
+ pool = descriptor_pool.Default()
+
+ # Salt the filename in the descriptor.
+ # This allows re-use of the filename by other proto messages if
+ # needed (e.g. if __all__ is not used).
+ salt = self._calculate_salt(new_class, fallback_salt)
+ self.descriptor.name = "{name}.proto".format(
+ name="_".join([self.descriptor.name[:-6], salt]).rstrip("_"),
+ )
+
+ # Add the file descriptor.
+ pool.Add(self.descriptor)
+
+ # Adding the file descriptor to the pool created a descriptor for
+ # each message; go back through our wrapper messages and associate
+ # them with the internal protobuf version.
+ for full_name, proto_plus_message in self.messages.items():
+ # Get the descriptor from the pool, and create the protobuf
+ # message based on it.
+ descriptor = pool.FindMessageTypeByName(full_name)
+ pb_message = reflection.GeneratedProtocolMessageType(
+ descriptor.name,
+ (message.Message,),
+ {"DESCRIPTOR": descriptor, "__module__": None},
+ )
+
+ # Register the message with the marshal so it is wrapped
+ # appropriately.
+ #
+ # We do this here (rather than at class creation) because it
+ # is not until this point that we have an actual protobuf
+ # message subclass, which is what we need to use.
+ proto_plus_message._meta._pb = pb_message
+ proto_plus_message._meta.marshal.register(
+ pb_message, MessageRule(pb_message, proto_plus_message)
+ )
+
+ # Iterate over any fields on the message and, if their type
+ # is a message still referenced as a string, resolve the reference.
+ for field in proto_plus_message._meta.fields.values():
+ if field.message and isinstance(field.message, str):
+ field.message = self.messages[field.message]
+ elif field.enum and isinstance(field.enum, str):
+ field.enum = self.enums[field.enum]
+
+ # Same thing for enums
+ for full_name, proto_plus_enum in self.enums.items():
+ descriptor = pool.FindEnumTypeByName(full_name)
+ proto_plus_enum._meta.pb = descriptor
+
+ # We no longer need to track this file's info; remove it from
+ # the module's registry and from this object.
+ self.registry.pop(self.name)
+
+ def ready(self, new_class):
+ """Return True if a file descriptor may added, False otherwise.
+
+ This determine if all the messages that we plan to create have been
+ created, as best as we are able.
+
+ Since messages depend on one another, we create descriptor protos
+ (which reference each other using strings) and wait until we have
+ built everything that is going to be in the module, and then
+ use the descriptor protos to instantiate the actual descriptors in
+ one fell swoop.
+
+ Args:
+ new_class (~.MessageMeta): The new class currently undergoing
+ creation.
+ """
+ # If there are any nested descriptors that have not been assigned to
+ # the descriptors that should contain them, then we are not ready.
+ if len(self.nested) or len(self.nested_enum):
+ return False
+
+ # If there are any unresolved fields (fields with a composite message
+ # declared as a string), ensure that the corresponding message is
+ # declared.
+ for field in self.unresolved_fields:
+ if (field.message and field.message not in self.messages) or (
+ field.enum and field.enum not in self.enums
+ ):
+ return False
+
+ # If the module in which this class is defined provides a
+ # __protobuf__ property, it may have a manifest.
+ #
+ # Do not generate the file descriptor until every member of the
+ # manifest has been populated.
+ module = inspect.getmodule(new_class)
+ manifest = self._get_remaining_manifest(new_class)
+
+ # We are ready if all members have been populated.
+ return all(hasattr(module, i) for i in manifest)
+
+ @property
+ def unresolved_fields(self):
+ """Return fields with referencing message types as strings."""
+ for proto_plus_message in self.messages.values():
+ for field in proto_plus_message._meta.fields.values():
+ if (field.message and isinstance(field.message, str)) or (
+ field.enum and isinstance(field.enum, str)
+ ):
+ yield field
diff --git a/packages/proto-plus/proto/_package_info.py b/packages/proto-plus/proto/_package_info.py
new file mode 100644
index 000000000000..75e89ebaac18
--- /dev/null
+++ b/packages/proto-plus/proto/_package_info.py
@@ -0,0 +1,50 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+
+from proto.marshal import Marshal
+
+
+def compile(name, attrs):
+ """Return the package and marshal to use.
+
+ Args:
+ name (str): The name of the new class, as sent to ``type.__new__``.
+ attrs (Mapping[str, Any]): The attrs for a new class, as sent
+ to ``type.__new__``
+
+ Returns:
+ Tuple[str, ~.Marshal]:
+ - The proto package, if any (empty string otherwise).
+ - The marshal object to use.
+ """
+ # Pull a reference to the module where this class is being
+ # declared.
+ module = sys.modules.get(attrs.get("__module__"))
+ module_name = module.__name__ if hasattr(module, __name__) else ""
+ proto_module = getattr(module, "__protobuf__", object())
+
+ # A package should be present; get the marshal from there.
+ # TODO: Revert to empty string as a package value after protobuf fix.
+ # When package is empty, upb based protobuf fails with an
+ # "TypeError: Couldn't build proto file into descriptor pool: invalid name: empty part ()' means"
+ # during an attempt to add to descriptor pool.
+ package = getattr(
+ proto_module, "package", module_name if module_name else "_default_package"
+ )
+ marshal = Marshal(name=getattr(proto_module, "marshal", package))
+
+ # Done; return the data.
+ return (package, marshal)
diff --git a/packages/proto-plus/proto/datetime_helpers.py b/packages/proto-plus/proto/datetime_helpers.py
new file mode 100644
index 000000000000..ffac4f47d95a
--- /dev/null
+++ b/packages/proto-plus/proto/datetime_helpers.py
@@ -0,0 +1,225 @@
+# Copyright 2017 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Helpers for :mod:`datetime`."""
+
+import calendar
+import datetime
+import re
+
+from google.protobuf import timestamp_pb2
+
+
+_UTC_EPOCH = datetime.datetime.fromtimestamp(0, datetime.timezone.utc)
+
+_RFC3339_MICROS = "%Y-%m-%dT%H:%M:%S.%fZ"
+_RFC3339_NO_FRACTION = "%Y-%m-%dT%H:%M:%S"
+# datetime.strptime cannot handle nanosecond precision: parse w/ regex
+_RFC3339_NANOS = re.compile(
+ r"""
+ (?P
+ \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2} # YYYY-MM-DDTHH:MM:SS
+ )
+ ( # Optional decimal part
+ \. # decimal point
+ (?P\d{1,9}) # nanoseconds, maybe truncated
+ )?
+ Z # Zulu
+""",
+ re.VERBOSE,
+)
+
+
+def _from_microseconds(value):
+ """Convert timestamp in microseconds since the unix epoch to datetime.
+
+ Args:
+ value (float): The timestamp to convert, in microseconds.
+
+ Returns:
+ datetime.datetime: The datetime object equivalent to the timestamp in
+ UTC.
+ """
+ return _UTC_EPOCH + datetime.timedelta(microseconds=value)
+
+
+def _to_rfc3339(value, ignore_zone=True):
+ """Convert a datetime to an RFC3339 timestamp string.
+
+ Args:
+ value (datetime.datetime):
+ The datetime object to be converted to a string.
+ ignore_zone (bool): If True, then the timezone (if any) of the
+ datetime object is ignored and the datetime is treated as UTC.
+
+ Returns:
+ str: The RFC3339 formatted string representing the datetime.
+ """
+ if not ignore_zone and value.tzinfo is not None:
+ # Convert to UTC and remove the time zone info.
+ value = value.replace(tzinfo=None) - value.utcoffset()
+
+ return value.strftime(_RFC3339_MICROS)
+
+
+class DatetimeWithNanoseconds(datetime.datetime):
+ """Track nanosecond in addition to normal datetime attrs.
+
+ Nanosecond can be passed only as a keyword argument.
+ """
+
+ __slots__ = ("_nanosecond",)
+
+ # pylint: disable=arguments-differ
+ def __new__(cls, *args, **kw):
+ nanos = kw.pop("nanosecond", 0)
+ if nanos > 0:
+ if "microsecond" in kw:
+ raise TypeError("Specify only one of 'microsecond' or 'nanosecond'")
+ kw["microsecond"] = nanos // 1000
+ inst = datetime.datetime.__new__(cls, *args, **kw)
+ inst._nanosecond = nanos or 0
+ return inst
+
+ # pylint: disable=arguments-differ
+ def replace(self, *args, **kw):
+ """Return a date with the same value, except for those parameters given
+ new values by whichever keyword arguments are specified. For example,
+ if d == date(2002, 12, 31), then
+ d.replace(day=26) == date(2002, 12, 26).
+ NOTE: nanosecond and microsecond are mutually exclusive arguments.
+ """
+
+ ms_provided = "microsecond" in kw
+ ns_provided = "nanosecond" in kw
+ provided_ns = kw.pop("nanosecond", 0)
+
+ prev_nanos = self.nanosecond
+
+ if ms_provided and ns_provided:
+ raise TypeError("Specify only one of 'microsecond' or 'nanosecond'")
+
+ if ns_provided:
+ # if nanos were provided, manipulate microsecond kw arg to super
+ kw["microsecond"] = provided_ns // 1000
+ inst = super().replace(*args, **kw)
+
+ if ms_provided:
+ # ms were provided, nanos are invalid, build from ms
+ inst._nanosecond = inst.microsecond * 1000
+ elif ns_provided:
+ # ns were provided, replace nanoseconds to match after calling super
+ inst._nanosecond = provided_ns
+ else:
+ # if neither ms or ns were provided, passthru previous nanos.
+ inst._nanosecond = prev_nanos
+
+ return inst
+
+ @property
+ def nanosecond(self):
+ """Read-only: nanosecond precision."""
+ return self._nanosecond or self.microsecond * 1000
+
+ def rfc3339(self):
+ """Return an RFC3339-compliant timestamp.
+
+ Returns:
+ (str): Timestamp string according to RFC3339 spec.
+ """
+ if self._nanosecond == 0:
+ return _to_rfc3339(self)
+ nanos = str(self._nanosecond).rjust(9, "0").rstrip("0")
+ return "{}.{}Z".format(self.strftime(_RFC3339_NO_FRACTION), nanos)
+
+ @classmethod
+ def from_rfc3339(cls, stamp):
+ """Parse RFC3339-compliant timestamp, preserving nanoseconds.
+
+ Args:
+ stamp (str): RFC3339 stamp, with up to nanosecond precision
+
+ Returns:
+ :class:`DatetimeWithNanoseconds`:
+ an instance matching the timestamp string
+
+ Raises:
+ ValueError: if `stamp` does not match the expected format
+ """
+ with_nanos = _RFC3339_NANOS.match(stamp)
+ if with_nanos is None:
+ raise ValueError(
+ "Timestamp: {}, does not match pattern: {}".format(
+ stamp, _RFC3339_NANOS.pattern
+ )
+ )
+ bare = datetime.datetime.strptime(
+ with_nanos.group("no_fraction"), _RFC3339_NO_FRACTION
+ )
+ fraction = with_nanos.group("nanos")
+ if fraction is None:
+ nanos = 0
+ else:
+ scale = 9 - len(fraction)
+ nanos = int(fraction) * (10**scale)
+ return cls(
+ bare.year,
+ bare.month,
+ bare.day,
+ bare.hour,
+ bare.minute,
+ bare.second,
+ nanosecond=nanos,
+ tzinfo=datetime.timezone.utc,
+ )
+
+ def timestamp_pb(self):
+ """Return a timestamp message.
+
+ Returns:
+ (:class:`~google.protobuf.timestamp_pb2.Timestamp`): Timestamp message
+ """
+ inst = (
+ self
+ if self.tzinfo is not None
+ else self.replace(tzinfo=datetime.timezone.utc)
+ )
+ delta = inst - _UTC_EPOCH
+ seconds = int(delta.total_seconds())
+ nanos = self._nanosecond or self.microsecond * 1000
+ return timestamp_pb2.Timestamp(seconds=seconds, nanos=nanos)
+
+ @classmethod
+ def from_timestamp_pb(cls, stamp):
+ """Parse RFC3339-compliant timestamp, preserving nanoseconds.
+
+ Args:
+ stamp (:class:`~google.protobuf.timestamp_pb2.Timestamp`): timestamp message
+
+ Returns:
+ :class:`DatetimeWithNanoseconds`:
+ an instance matching the timestamp message
+ """
+ microseconds = int(stamp.seconds * 1e6)
+ bare = _from_microseconds(microseconds)
+ return cls(
+ bare.year,
+ bare.month,
+ bare.day,
+ bare.hour,
+ bare.minute,
+ bare.second,
+ nanosecond=stamp.nanos,
+ tzinfo=datetime.timezone.utc,
+ )
diff --git a/packages/proto-plus/proto/enums.py b/packages/proto-plus/proto/enums.py
new file mode 100644
index 000000000000..d3ab6b792023
--- /dev/null
+++ b/packages/proto-plus/proto/enums.py
@@ -0,0 +1,165 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import enum
+
+from google.protobuf import descriptor_pb2
+
+from proto import _file_info
+from proto import _package_info
+from proto.marshal.rules.enums import EnumRule
+
+
+class ProtoEnumMeta(enum.EnumMeta):
+ """A metaclass for building and registering protobuf enums."""
+
+ def __new__(mcls, name, bases, attrs):
+ # Do not do any special behavior for `proto.Enum` itself.
+ if bases[0] == enum.IntEnum:
+ return super().__new__(mcls, name, bases, attrs)
+
+ # Get the essential information about the proto package, and where
+ # this component belongs within the file.
+ package, marshal = _package_info.compile(name, attrs)
+
+ # Determine the local path of this proto component within the file.
+ local_path = tuple(attrs.get("__qualname__", name).split("."))
+
+ # Sanity check: We get the wrong full name if a class is declared
+ # inside a function local scope; correct this.
+ if "" in local_path:
+ ix = local_path.index("")
+ local_path = local_path[: ix - 1] + local_path[ix + 1 :]
+
+ # Determine the full name in protocol buffers.
+ full_name = ".".join((package,) + local_path).lstrip(".")
+ filename = _file_info._FileInfo.proto_file_name(
+ attrs.get("__module__", name.lower())
+ )
+
+ # Retrieve any enum options.
+ # We expect something that looks like an EnumOptions message,
+ # either an actual instance or a dict-like representation.
+ pb_options = "_pb_options"
+ opts = attrs.pop(pb_options, {})
+ # This is the only portable way to remove the _pb_options name
+ # from the enum attrs.
+ # In 3.7 onwards, we can define an _ignore_ attribute and do some
+ # mucking around with that.
+ if pb_options in attrs._member_names:
+ if isinstance(attrs._member_names, list):
+ idx = attrs._member_names.index(pb_options)
+ attrs._member_names.pop(idx)
+ elif isinstance(attrs._member_names, set): # PyPy
+ attrs._member_names.discard(pb_options)
+ else: # Python 3.11.0b3
+ del attrs._member_names[pb_options]
+
+ # Make the descriptor.
+ enum_desc = descriptor_pb2.EnumDescriptorProto(
+ name=name,
+ # Note: the superclass ctor removes the variants, so get them now.
+ # Note: proto3 requires that the first variant value be zero.
+ value=sorted(
+ (
+ descriptor_pb2.EnumValueDescriptorProto(name=name, number=number)
+ # Minor hack to get all the enum variants out.
+ # Use the `_member_names` property to get only the enum members
+ # See https://github.com/googleapis/proto-plus-python/issues/490
+ for name, number in attrs.items()
+ if name in attrs._member_names and isinstance(number, int)
+ ),
+ key=lambda v: v.number,
+ ),
+ options=opts,
+ )
+
+ file_info = _file_info._FileInfo.maybe_add_descriptor(filename, package)
+ if len(local_path) == 1:
+ file_info.descriptor.enum_type.add().MergeFrom(enum_desc)
+ else:
+ file_info.nested_enum[local_path] = enum_desc
+
+ # Run the superclass constructor.
+ cls = super().__new__(mcls, name, bases, attrs)
+
+ # We can't just add a "_meta" element to attrs because the Enum
+ # machinery doesn't know what to do with a non-int value.
+ # The pb is set later, in generate_file_pb
+ cls._meta = _EnumInfo(full_name=full_name, pb=None)
+
+ file_info.enums[full_name] = cls
+
+ # Register the enum with the marshal.
+ marshal.register(cls, EnumRule(cls))
+
+ # Generate the descriptor for the file if it is ready.
+ if file_info.ready(new_class=cls):
+ file_info.generate_file_pb(new_class=cls, fallback_salt=full_name)
+
+ # Done; return the class.
+ return cls
+
+
+class Enum(enum.IntEnum, metaclass=ProtoEnumMeta):
+ """A enum object that also builds a protobuf enum descriptor."""
+
+ def _comparable(self, other):
+ # Avoid 'isinstance' to prevent other IntEnums from matching
+ return type(other) in (type(self), int)
+
+ def __hash__(self):
+ return hash(self.value)
+
+ def __eq__(self, other):
+ if not self._comparable(other):
+ return NotImplemented
+
+ return self.value == int(other)
+
+ def __ne__(self, other):
+ if not self._comparable(other):
+ return NotImplemented
+
+ return self.value != int(other)
+
+ def __lt__(self, other):
+ if not self._comparable(other):
+ return NotImplemented
+
+ return self.value < int(other)
+
+ def __le__(self, other):
+ if not self._comparable(other):
+ return NotImplemented
+
+ return self.value <= int(other)
+
+ def __ge__(self, other):
+ if not self._comparable(other):
+ return NotImplemented
+
+ return self.value >= int(other)
+
+ def __gt__(self, other):
+ if not self._comparable(other):
+ return NotImplemented
+
+ return self.value > int(other)
+
+
+class _EnumInfo:
+ def __init__(self, *, full_name: str, pb):
+ self.full_name = full_name
+ self.pb = pb
diff --git a/packages/proto-plus/proto/fields.py b/packages/proto-plus/proto/fields.py
new file mode 100644
index 000000000000..6f5b64521619
--- /dev/null
+++ b/packages/proto-plus/proto/fields.py
@@ -0,0 +1,165 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from enum import EnumMeta
+
+from google.protobuf import descriptor_pb2
+from google.protobuf.internal.enum_type_wrapper import EnumTypeWrapper
+
+from proto.primitives import ProtoType
+
+
+class Field:
+ """A representation of a type of field in protocol buffers."""
+
+ # Fields are NOT repeated nor maps.
+ # The RepeatedField overrides this values.
+ repeated = False
+
+ def __init__(
+ self,
+ proto_type,
+ *,
+ number: int,
+ message=None,
+ enum=None,
+ oneof: str = None,
+ json_name: str = None,
+ optional: bool = False
+ ):
+ # This class is not intended to stand entirely alone;
+ # data is augmented by the metaclass for Message.
+ self.mcls_data = None
+ self.parent = None
+
+ # If the proto type sent is an object or a string, it is really
+ # a message or enum.
+ if not isinstance(proto_type, int):
+ # Note: We only support the "shortcut syntax" for enums
+ # when receiving the actual class.
+ if isinstance(proto_type, (EnumMeta, EnumTypeWrapper)):
+ enum = proto_type
+ proto_type = ProtoType.ENUM
+ else:
+ message = proto_type
+ proto_type = ProtoType.MESSAGE
+
+ # Save the direct arguments.
+ self.number = number
+ self.proto_type = proto_type
+ self.message = message
+ self.enum = enum
+ self.json_name = json_name
+ self.optional = optional
+ self.oneof = oneof
+
+ # Once the descriptor is accessed the first time, cache it.
+ # This is important because in rare cases the message or enum
+ # types are written later.
+ self._descriptor = None
+
+ @property
+ def descriptor(self):
+ """Return the descriptor for the field."""
+ if not self._descriptor:
+ # Resolve the message type, if any, to a string.
+ type_name = None
+ if isinstance(self.message, str):
+ if not self.message.startswith(self.package):
+ self.message = "{package}.{name}".format(
+ package=self.package,
+ name=self.message,
+ )
+ type_name = self.message
+ elif self.message:
+ type_name = (
+ self.message.DESCRIPTOR.full_name
+ if hasattr(self.message, "DESCRIPTOR")
+ else self.message._meta.full_name
+ )
+ elif isinstance(self.enum, str):
+ if not self.enum.startswith(self.package):
+ self.enum = "{package}.{name}".format(
+ package=self.package,
+ name=self.enum,
+ )
+ type_name = self.enum
+ elif self.enum:
+ type_name = (
+ self.enum.DESCRIPTOR.full_name
+ if hasattr(self.enum, "DESCRIPTOR")
+ else self.enum._meta.full_name
+ )
+
+ # Set the descriptor.
+ self._descriptor = descriptor_pb2.FieldDescriptorProto(
+ name=self.name,
+ number=self.number,
+ label=3 if self.repeated else 1,
+ type=self.proto_type,
+ type_name=type_name,
+ json_name=self.json_name,
+ proto3_optional=self.optional,
+ )
+
+ # Return the descriptor.
+ return self._descriptor
+
+ @property
+ def name(self) -> str:
+ """Return the name of the field."""
+ return self.mcls_data["name"]
+
+ @property
+ def package(self) -> str:
+ """Return the package of the field."""
+ return self.mcls_data["package"]
+
+ @property
+ def pb_type(self):
+ """Return the composite type of the field, or the primitive type if a primitive."""
+ # For enums, return the Python enum.
+ if self.enum:
+ return self.enum
+
+ # For primitive fields, we still want to know
+ # what the type is.
+ if not self.message:
+ return self.proto_type
+
+ # Return the internal protobuf message.
+ if hasattr(self.message, "_meta"):
+ return self.message.pb()
+ return self.message
+
+
+class RepeatedField(Field):
+ """A representation of a repeated field in protocol buffers."""
+
+ repeated = True
+
+
+class MapField(Field):
+ """A representation of a map field in protocol buffers."""
+
+ def __init__(self, key_type, value_type, *, number: int, message=None, enum=None):
+ super().__init__(value_type, number=number, message=message, enum=enum)
+ self.map_key_type = key_type
+
+
+__all__ = (
+ "Field",
+ "MapField",
+ "RepeatedField",
+)
diff --git a/packages/proto-plus/proto/marshal/__init__.py b/packages/proto-plus/proto/marshal/__init__.py
new file mode 100644
index 000000000000..621ea3695f93
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/__init__.py
@@ -0,0 +1,18 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .marshal import Marshal
+
+
+__all__ = ("Marshal",)
diff --git a/packages/proto-plus/proto/marshal/collections/__init__.py b/packages/proto-plus/proto/marshal/collections/__init__.py
new file mode 100644
index 000000000000..4b80a546c26a
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/collections/__init__.py
@@ -0,0 +1,24 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .maps import MapComposite
+from .repeated import Repeated
+from .repeated import RepeatedComposite
+
+
+__all__ = (
+ "MapComposite",
+ "Repeated",
+ "RepeatedComposite",
+)
diff --git a/packages/proto-plus/proto/marshal/collections/maps.py b/packages/proto-plus/proto/marshal/collections/maps.py
new file mode 100644
index 000000000000..3c4857161458
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/collections/maps.py
@@ -0,0 +1,82 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import collections
+
+from proto.utils import cached_property
+from google.protobuf.message import Message
+
+
+class MapComposite(collections.abc.MutableMapping):
+ """A view around a mutable sequence in protocol buffers.
+
+ This implements the full Python MutableMapping interface, but all methods
+ modify the underlying field container directly.
+ """
+
+ @cached_property
+ def _pb_type(self):
+ """Return the protocol buffer type for this sequence."""
+ # Huzzah, another hack. Still less bad than RepeatedComposite.
+ return type(self.pb.GetEntryClass()().value)
+
+ def __init__(self, sequence, *, marshal):
+ """Initialize a wrapper around a protobuf map.
+
+ Args:
+ sequence: A protocol buffers map.
+ marshal (~.MarshalRegistry): An instantiated marshal, used to
+ convert values going to and from this map.
+ """
+ self._pb = sequence
+ self._marshal = marshal
+
+ def __contains__(self, key):
+ # Protocol buffers is so permissive that querying for the existence
+ # of a key will in of itself create it.
+ #
+ # By taking a tuple of the keys and querying that, we avoid sending
+ # the lookup to protocol buffers and therefore avoid creating the key.
+ return key in tuple(self.keys())
+
+ def __getitem__(self, key):
+ # We handle raising KeyError ourselves, because otherwise protocol
+ # buffers will create the key if it does not exist.
+ if key not in self:
+ raise KeyError(key)
+ return self._marshal.to_python(self._pb_type, self.pb[key])
+
+ def __setitem__(self, key, value):
+ pb_value = self._marshal.to_proto(self._pb_type, value, strict=True)
+ # Directly setting a key is not allowed; however, protocol buffers
+ # is so permissive that querying for the existence of a key will in
+ # of itself create it.
+ #
+ # Therefore, we create a key that way (clearing any fields that may
+ # be set) and then merge in our values.
+ self.pb[key].Clear()
+ self.pb[key].MergeFrom(pb_value)
+
+ def __delitem__(self, key):
+ self.pb.pop(key)
+
+ def __len__(self):
+ return len(self.pb)
+
+ def __iter__(self):
+ return iter(self.pb)
+
+ @property
+ def pb(self):
+ return self._pb
diff --git a/packages/proto-plus/proto/marshal/collections/repeated.py b/packages/proto-plus/proto/marshal/collections/repeated.py
new file mode 100644
index 000000000000..a6560411cd71
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/collections/repeated.py
@@ -0,0 +1,189 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import collections
+import copy
+from typing import Iterable
+
+from proto.utils import cached_property
+
+
+class Repeated(collections.abc.MutableSequence):
+ """A view around a mutable sequence in protocol buffers.
+
+ This implements the full Python MutableSequence interface, but all methods
+ modify the underlying field container directly.
+ """
+
+ def __init__(self, sequence, *, marshal, proto_type=None):
+ """Initialize a wrapper around a protobuf repeated field.
+
+ Args:
+ sequence: A protocol buffers repeated field.
+ marshal (~.MarshalRegistry): An instantiated marshal, used to
+ convert values going to and from this map.
+ """
+ self._pb = sequence
+ self._marshal = marshal
+ self._proto_type = proto_type
+
+ def __copy__(self):
+ """Copy this object and return the copy."""
+ return type(self)(self.pb[:], marshal=self._marshal)
+
+ def __delitem__(self, key):
+ """Delete the given item."""
+ del self.pb[key]
+
+ def __eq__(self, other):
+ if hasattr(other, "pb"):
+ return tuple(self.pb) == tuple(other.pb)
+ return tuple(self.pb) == tuple(other) if isinstance(other, Iterable) else False
+
+ def __getitem__(self, key):
+ """Return the given item."""
+ return self.pb[key]
+
+ def __len__(self):
+ """Return the length of the sequence."""
+ return len(self.pb)
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __repr__(self):
+ return repr([*self])
+
+ def __setitem__(self, key, value):
+ self.pb[key] = value
+
+ def insert(self, index: int, value):
+ """Insert ``value`` in the sequence before ``index``."""
+ self.pb.insert(index, value)
+
+ def sort(self, *, key: str = None, reverse: bool = False):
+ """Stable sort *IN PLACE*."""
+ self.pb.sort(key=key, reverse=reverse)
+
+ @property
+ def pb(self):
+ return self._pb
+
+
+class RepeatedComposite(Repeated):
+ """A view around a mutable sequence of messages in protocol buffers.
+
+ This implements the full Python MutableSequence interface, but all methods
+ modify the underlying field container directly.
+ """
+
+ @cached_property
+ def _pb_type(self):
+ """Return the protocol buffer type for this sequence."""
+ # Provide the marshal-given proto_type, if any.
+ # Used for RepeatedComposite of Enum.
+ if self._proto_type is not None:
+ return self._proto_type
+
+ # There is no public-interface mechanism to determine the type
+ # of what should go in the list (and the C implementation seems to
+ # have no exposed mechanism at all).
+ #
+ # If the list has members, use the existing list members to
+ # determine the type.
+ if len(self.pb) > 0:
+ return type(self.pb[0])
+
+ # We have no members in the list, so we get the type from the attributes.
+ if hasattr(self.pb, "_message_descriptor") and hasattr(
+ self.pb._message_descriptor, "_concrete_class"
+ ):
+ return self.pb._message_descriptor._concrete_class
+
+ # Fallback logic in case attributes are not available
+ # In order to get the type, we create a throw-away copy and add a
+ # blank member to it.
+ canary = copy.deepcopy(self.pb).add()
+ return type(canary)
+
+ def __eq__(self, other):
+ if super().__eq__(other):
+ return True
+ return (
+ tuple([i for i in self]) == tuple(other)
+ if isinstance(other, Iterable)
+ else False
+ )
+
+ def __getitem__(self, key):
+ return self._marshal.to_python(self._pb_type, self.pb[key])
+
+ def __setitem__(self, key, value):
+ # The underlying protocol buffer does not define __setitem__, so we
+ # have to implement all the operations on our own.
+
+ # If ``key`` is an integer, as in list[index] = value:
+ if isinstance(key, int):
+ if -len(self) <= key < len(self):
+ self.pop(key) # Delete the old item.
+ self.insert(key, value) # Insert the new item in its place.
+ else:
+ raise IndexError("list assignment index out of range")
+
+ # If ``key`` is a slice object, as in list[start:stop:step] = [values]:
+ elif isinstance(key, slice):
+ start, stop, step = key.indices(len(self))
+
+ if not isinstance(value, collections.abc.Iterable):
+ raise TypeError("can only assign an iterable")
+
+ if step == 1: # Is not an extended slice.
+ # Assign all the new values to the sliced part, replacing the
+ # old values, if any, and unconditionally inserting those
+ # values whose indices already exceed the slice length.
+ for index, item in enumerate(value):
+ if start + index < stop:
+ self.pop(start + index)
+ self.insert(start + index, item)
+
+ # If there are less values than the length of the slice, remove
+ # the remaining elements so that the slice adapts to the
+ # newly provided values.
+ for _ in range(stop - start - len(value)):
+ self.pop(start + len(value))
+
+ else: # Is an extended slice.
+ indices = range(start, stop, step)
+
+ if len(value) != len(indices): # XXX: Use PEP 572 on 3.8+
+ raise ValueError(
+ f"attempt to assign sequence of size "
+ f"{len(value)} to extended slice of size "
+ f"{len(indices)}"
+ )
+
+ # Assign each value to its index, calling this function again
+ # with individual integer indexes that get processed above.
+ for index, item in zip(indices, value):
+ self[index] = item
+
+ else:
+ raise TypeError(
+ f"list indices must be integers or slices, not {type(key).__name__}"
+ )
+
+ def insert(self, index: int, value):
+ """Insert ``value`` in the sequence before ``index``."""
+ pb_value = self._marshal.to_proto(self._pb_type, value)
+ self.pb.insert(index, pb_value)
diff --git a/packages/proto-plus/proto/marshal/compat.py b/packages/proto-plus/proto/marshal/compat.py
new file mode 100644
index 000000000000..b393acf5a447
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/compat.py
@@ -0,0 +1,64 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This file pulls in the container types from internal protocol buffers,
+# and exports the types available.
+#
+# If the C extensions were not installed, then their container types will
+# not be included.
+
+from google.protobuf.internal import containers
+
+# Import all message types to ensure that pyext types are recognized
+# when upb types exist. Conda's protobuf defaults to pyext despite upb existing.
+# See https://github.com/googleapis/proto-plus-python/issues/470
+try:
+ from google._upb import _message as _message_upb
+except ImportError:
+ _message_upb = None
+
+try:
+ from google.protobuf.pyext import _message as _message_pyext
+except ImportError:
+ _message_pyext = None
+
+
+repeated_composite_types = (containers.RepeatedCompositeFieldContainer,)
+repeated_scalar_types = (containers.RepeatedScalarFieldContainer,)
+map_composite_types = (containers.MessageMap,)
+
+# In `proto/marshal.py`, for compatibility with protobuf 5.x,
+# we'll use `map_composite_type_names` to check whether
+# the name of the class of a protobuf type is
+# `MessageMapContainer`, and, if `True`, return a MapComposite.
+# See https://github.com/protocolbuffers/protobuf/issues/16596
+map_composite_type_names = ("MessageMapContainer",)
+
+for message in [_message_upb, _message_pyext]:
+ if message:
+ repeated_composite_types += (message.RepeatedCompositeContainer,)
+ repeated_scalar_types += (message.RepeatedScalarContainer,)
+
+ try:
+ map_composite_types += (message.MessageMapContainer,)
+ except AttributeError:
+ # The `MessageMapContainer` attribute is not available in Protobuf 5.x+
+ pass
+
+__all__ = (
+ "repeated_composite_types",
+ "repeated_scalar_types",
+ "map_composite_types",
+ "map_composite_type_names",
+)
diff --git a/packages/proto-plus/proto/marshal/marshal.py b/packages/proto-plus/proto/marshal/marshal.py
new file mode 100644
index 000000000000..d278421a5797
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/marshal.py
@@ -0,0 +1,297 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import abc
+import enum
+
+from google.protobuf import message
+from google.protobuf import duration_pb2
+from google.protobuf import timestamp_pb2
+from google.protobuf import field_mask_pb2
+from google.protobuf import struct_pb2
+from google.protobuf import wrappers_pb2
+
+from proto.marshal import compat
+from proto.marshal.collections import MapComposite
+from proto.marshal.collections import Repeated
+from proto.marshal.collections import RepeatedComposite
+
+from proto.marshal.rules import bytes as pb_bytes
+from proto.marshal.rules import stringy_numbers
+from proto.marshal.rules import dates
+from proto.marshal.rules import struct
+from proto.marshal.rules import wrappers
+from proto.marshal.rules import field_mask
+from proto.primitives import ProtoType
+
+
+class Rule(abc.ABC):
+ """Abstract class definition for marshal rules."""
+
+ @classmethod
+ def __subclasshook__(cls, C):
+ if hasattr(C, "to_python") and hasattr(C, "to_proto"):
+ return True
+ return NotImplemented
+
+
+class BaseMarshal:
+ """The base class to translate between protobuf and Python classes.
+
+ Protocol buffers defines many common types (e.g. Timestamp, Duration)
+ which also exist in the Python standard library. The marshal essentially
+ translates between these: it keeps a registry of common protocol buffers
+ and their Python representations, and translates back and forth.
+
+ The protocol buffer class is always the "key" in this relationship; when
+ presenting a message, the declared field types are used to determine
+ whether a value should be transformed into another class. Similarly,
+ when accepting a Python value (when setting a field, for example),
+ the declared field type is still used. This means that, if appropriate,
+ multiple protocol buffer types may use the same Python type.
+
+ The primary implementation of this is :class:`Marshal`, which should
+ usually be used instead of this class directly.
+ """
+
+ def __init__(self):
+ self._rules = {}
+ self._noop = NoopRule()
+ self.reset()
+
+ def register(self, proto_type: type, rule: Rule = None):
+ """Register a rule against the given ``proto_type``.
+
+ This function expects a ``proto_type`` (the descriptor class) and
+ a ``rule``; an object with a ``to_python`` and ``to_proto`` method.
+ Each method should return the appropriate Python or protocol buffer
+ type, and be idempotent (e.g. accept either type as input).
+
+ This function can also be used as a decorator::
+
+ @marshal.register(timestamp_pb2.Timestamp)
+ class TimestampRule:
+ ...
+
+ In this case, the class will be initialized for you with zero
+ arguments.
+
+ Args:
+ proto_type (type): A protocol buffer message type.
+ rule: A marshal object
+ """
+ # If a rule was provided, register it and be done.
+ if rule:
+ # Ensure the rule implements Rule.
+ if not isinstance(rule, Rule):
+ raise TypeError(
+ "Marshal rule instances must implement "
+ "`to_proto` and `to_python` methods."
+ )
+
+ # Register the rule.
+ self._rules[proto_type] = rule
+ return
+
+ # Create an inner function that will register an instance of the
+ # marshal class to this object's registry, and return it.
+ def register_rule_class(rule_class: type):
+ # Ensure the rule class is a valid rule.
+ if not issubclass(rule_class, Rule):
+ raise TypeError(
+ "Marshal rule subclasses must implement "
+ "`to_proto` and `to_python` methods."
+ )
+
+ # Register the rule class.
+ self._rules[proto_type] = rule_class()
+ return rule_class
+
+ return register_rule_class
+
+ def reset(self):
+ """Reset the registry to its initial state."""
+ self._rules.clear()
+
+ # Register date and time wrappers.
+ self.register(timestamp_pb2.Timestamp, dates.TimestampRule())
+ self.register(duration_pb2.Duration, dates.DurationRule())
+
+ # Register FieldMask wrappers.
+ self.register(field_mask_pb2.FieldMask, field_mask.FieldMaskRule())
+
+ # Register nullable primitive wrappers.
+ self.register(wrappers_pb2.BoolValue, wrappers.BoolValueRule())
+ self.register(wrappers_pb2.BytesValue, wrappers.BytesValueRule())
+ self.register(wrappers_pb2.DoubleValue, wrappers.DoubleValueRule())
+ self.register(wrappers_pb2.FloatValue, wrappers.FloatValueRule())
+ self.register(wrappers_pb2.Int32Value, wrappers.Int32ValueRule())
+ self.register(wrappers_pb2.Int64Value, wrappers.Int64ValueRule())
+ self.register(wrappers_pb2.StringValue, wrappers.StringValueRule())
+ self.register(wrappers_pb2.UInt32Value, wrappers.UInt32ValueRule())
+ self.register(wrappers_pb2.UInt64Value, wrappers.UInt64ValueRule())
+
+ # Register the google.protobuf.Struct wrappers.
+ #
+ # These are aware of the marshal that created them, because they
+ # create RepeatedComposite and MapComposite instances directly and
+ # need to pass the marshal to them.
+ self.register(struct_pb2.Value, struct.ValueRule(marshal=self))
+ self.register(struct_pb2.ListValue, struct.ListValueRule(marshal=self))
+ self.register(struct_pb2.Struct, struct.StructRule(marshal=self))
+
+ # Special case for bytes to allow base64 encode/decode
+ self.register(ProtoType.BYTES, pb_bytes.BytesRule())
+
+ # Special case for int64 from strings because of dict round trip.
+ # See https://github.com/protocolbuffers/protobuf/issues/2679
+ for rule_class in stringy_numbers.STRINGY_NUMBER_RULES:
+ self.register(rule_class._proto_type, rule_class())
+
+ def get_rule(self, proto_type):
+ # Rules are needed to convert values between proto-plus and pb.
+ # Retrieve the rule for the specified proto type.
+ # The NoopRule will be used when a rule is not found.
+ rule = self._rules.get(proto_type, self._noop)
+
+ # If we don't find a rule, also check under `_instances`
+ # in case there is a rule in another package.
+ # See https://github.com/googleapis/proto-plus-python/issues/349
+ if rule == self._noop and hasattr(self, "_instances"):
+ for _, instance in self._instances.items():
+ rule = instance._rules.get(proto_type, self._noop)
+ if rule != self._noop:
+ break
+ return rule
+
+ def to_python(self, proto_type, value, *, absent: bool = None):
+ # Internal protobuf has its own special type for lists of values.
+ # Return a view around it that implements MutableSequence.
+ value_type = type(value) # Minor performance boost over isinstance
+ if value_type in compat.repeated_composite_types:
+ return RepeatedComposite(value, marshal=self)
+ if value_type in compat.repeated_scalar_types:
+ if isinstance(proto_type, type):
+ return RepeatedComposite(value, marshal=self, proto_type=proto_type)
+ else:
+ return Repeated(value, marshal=self)
+
+ # Same thing for maps of messages.
+ # See https://github.com/protocolbuffers/protobuf/issues/16596
+ # We need to look up the name of the type in compat.map_composite_type_names
+ # as class `MessageMapContainer` is no longer exposed
+ # This is done to avoid taking a breaking change in proto-plus.
+ if (
+ value_type in compat.map_composite_types
+ or value_type.__name__ in compat.map_composite_type_names
+ ):
+ return MapComposite(value, marshal=self)
+ return self.get_rule(proto_type=proto_type).to_python(value, absent=absent)
+
+ def to_proto(self, proto_type, value, *, strict: bool = False):
+ # The protos in google/protobuf/struct.proto are exceptional cases,
+ # because they can and should represent themselves as lists and dicts.
+ # These cases are handled in their rule classes.
+ if proto_type not in (
+ struct_pb2.Value,
+ struct_pb2.ListValue,
+ struct_pb2.Struct,
+ ):
+ # For our repeated and map view objects, simply return the
+ # underlying pb.
+ if isinstance(value, (Repeated, MapComposite)):
+ return value.pb
+
+ # Convert lists and tuples recursively.
+ if isinstance(value, (list, tuple)):
+ return type(value)(self.to_proto(proto_type, i) for i in value)
+
+ # Convert dictionaries recursively when the proto type is a map.
+ # This is slightly more complicated than converting a list or tuple
+ # because we have to step through the magic that protocol buffers does.
+ #
+ # Essentially, a type of map will show up here as
+ # a FoosEntry with a `key` field, `value` field, and a `map_entry`
+ # annotation. We need to do the conversion based on the `value`
+ # field's type.
+ if isinstance(value, dict) and (
+ proto_type.DESCRIPTOR.has_options
+ and proto_type.DESCRIPTOR.GetOptions().map_entry
+ ):
+ recursive_type = type(proto_type().value)
+ return {k: self.to_proto(recursive_type, v) for k, v in value.items()}
+
+ pb_value = self.get_rule(proto_type=proto_type).to_proto(value)
+
+ # Sanity check: If we are in strict mode, did we get the value we want?
+ if strict and not isinstance(pb_value, proto_type):
+ raise TypeError(
+ "Parameter must be instance of the same class; "
+ "expected {expected}, got {got}".format(
+ expected=proto_type.__name__,
+ got=pb_value.__class__.__name__,
+ ),
+ )
+ # Return the final value.
+ return pb_value
+
+
+class Marshal(BaseMarshal):
+ """The translator between protocol buffer and Python instances.
+
+ The bulk of the implementation is in :class:`BaseMarshal`. This class
+ adds identity tracking: multiple instantiations of :class:`Marshal` with
+ the same name will provide the same instance.
+ """
+
+ _instances = {}
+
+ def __new__(cls, *, name: str):
+ """Create a marshal instance.
+
+ Args:
+ name (str): The name of the marshal. Instantiating multiple
+ marshals with the same ``name`` argument will provide the
+ same marshal each time.
+ """
+ klass = cls._instances.get(name)
+ if klass is None:
+ klass = cls._instances[name] = super().__new__(cls)
+
+ return klass
+
+ def __init__(self, *, name: str):
+ """Instantiate a marshal.
+
+ Args:
+ name (str): The name of the marshal. Instantiating multiple
+ marshals with the same ``name`` argument will provide the
+ same marshal each time.
+ """
+ self._name = name
+ if not hasattr(self, "_rules"):
+ super().__init__()
+
+
+class NoopRule:
+ """A catch-all rule that does nothing."""
+
+ def to_python(self, pb_value, *, absent: bool = None):
+ return pb_value
+
+ def to_proto(self, value):
+ return value
+
+
+__all__ = ("Marshal",)
diff --git a/packages/proto-plus/proto/marshal/rules/__init__.py b/packages/proto-plus/proto/marshal/rules/__init__.py
new file mode 100644
index 000000000000..b0c7da3d7725
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/rules/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/packages/proto-plus/proto/marshal/rules/bytes.py b/packages/proto-plus/proto/marshal/rules/bytes.py
new file mode 100644
index 000000000000..080b0a03d4a0
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/rules/bytes.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import base64
+
+
+class BytesRule:
+ """A marshal between Python strings and protobuf bytes.
+
+ Note: this conversion is asymmetric because Python does have a bytes type.
+ It is sometimes necessary to convert proto bytes fields to strings, e.g. for
+ JSON encoding, marshalling a message to a dict. Because bytes fields can
+ represent arbitrary data, bytes fields are base64 encoded when they need to
+ be represented as strings.
+
+ It is necessary to have the conversion be bidirectional, i.e.
+ my_message == MyMessage(MyMessage.to_dict(my_message))
+
+ To accomplish this, we need to intercept assignments from strings and
+ base64 decode them back into bytes.
+ """
+
+ def to_python(self, value, *, absent: bool = None):
+ return value
+
+ def to_proto(self, value):
+ if isinstance(value, str):
+ value = value.encode("utf-8")
+ value += b"=" * (4 - len(value) % 4) # padding
+ value = base64.urlsafe_b64decode(value)
+
+ return value
diff --git a/packages/proto-plus/proto/marshal/rules/dates.py b/packages/proto-plus/proto/marshal/rules/dates.py
new file mode 100644
index 000000000000..33d12829b3cc
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/rules/dates.py
@@ -0,0 +1,85 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from datetime import datetime
+from datetime import timedelta
+from datetime import timezone
+
+from google.protobuf import duration_pb2
+from google.protobuf import timestamp_pb2
+from proto import datetime_helpers, utils
+
+
+class TimestampRule:
+ """A marshal between Python datetimes and protobuf timestamps.
+
+ Note: Python datetimes are less precise than protobuf datetimes
+ (microsecond vs. nanosecond level precision). If nanosecond-level
+ precision matters, it is recommended to interact with the internal
+ proto directly.
+ """
+
+ def to_python(
+ self, value, *, absent: bool = None
+ ) -> datetime_helpers.DatetimeWithNanoseconds:
+ if isinstance(value, timestamp_pb2.Timestamp):
+ if absent:
+ return None
+ return datetime_helpers.DatetimeWithNanoseconds.from_timestamp_pb(value)
+ return value
+
+ def to_proto(self, value) -> timestamp_pb2.Timestamp:
+ if isinstance(value, datetime_helpers.DatetimeWithNanoseconds):
+ return value.timestamp_pb()
+ if isinstance(value, datetime):
+ return timestamp_pb2.Timestamp(
+ seconds=int(value.timestamp()),
+ nanos=value.microsecond * 1000,
+ )
+ if isinstance(value, str):
+ timestamp_value = timestamp_pb2.Timestamp()
+ timestamp_value.FromJsonString(value=value)
+ return timestamp_value
+ return value
+
+
+class DurationRule:
+ """A marshal between Python timedeltas and protobuf durations.
+
+ Note: Python timedeltas are less precise than protobuf durations
+ (microsecond vs. nanosecond level precision). If nanosecond-level
+ precision matters, it is recommended to interact with the internal
+ proto directly.
+ """
+
+ def to_python(self, value, *, absent: bool = None) -> timedelta:
+ if isinstance(value, duration_pb2.Duration):
+ return timedelta(
+ days=value.seconds // 86400,
+ seconds=value.seconds % 86400,
+ microseconds=value.nanos // 1000,
+ )
+ return value
+
+ def to_proto(self, value) -> duration_pb2.Duration:
+ if isinstance(value, timedelta):
+ return duration_pb2.Duration(
+ seconds=value.days * 86400 + value.seconds,
+ nanos=value.microseconds * 1000,
+ )
+ if isinstance(value, str):
+ duration_value = duration_pb2.Duration()
+ duration_value.FromJsonString(value=value)
+ return duration_value
+ return value
diff --git a/packages/proto-plus/proto/marshal/rules/enums.py b/packages/proto-plus/proto/marshal/rules/enums.py
new file mode 100644
index 000000000000..9cfc312764b6
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/rules/enums.py
@@ -0,0 +1,59 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Type
+import enum
+import warnings
+
+
+class EnumRule:
+ """A marshal for converting between integer values and enum values."""
+
+ def __init__(self, enum_class: Type[enum.IntEnum]):
+ self._enum = enum_class
+
+ def to_python(self, value, *, absent: bool = None):
+ if isinstance(value, int) and not isinstance(value, self._enum):
+ try:
+ # Coerce the int on the wire to the enum value.
+ return self._enum(value)
+ except ValueError:
+ # Since it is possible to add values to enums, we do
+ # not want to flatly error on this.
+ #
+ # However, it is useful to make some noise about it so
+ # the user realizes that an unexpected value came along.
+ warnings.warn(
+ "Unrecognized {name} enum value: {value}".format(
+ name=self._enum.__name__,
+ value=value,
+ )
+ )
+ return value
+
+ def to_proto(self, value):
+ # Accept enum values and coerce to the pure integer.
+ # This is not strictly necessary (protocol buffers can take these
+ # objects as they subclass int) but nevertheless seems like the
+ # right thing to do.
+ if isinstance(value, self._enum):
+ return value.value
+
+ # If a string is provided that matches an enum value, coerce it
+ # to the enum value.
+ if isinstance(value, str):
+ return self._enum[value].value
+
+ # We got a pure integer; pass it on.
+ return value
diff --git a/packages/proto-plus/proto/marshal/rules/field_mask.py b/packages/proto-plus/proto/marshal/rules/field_mask.py
new file mode 100644
index 000000000000..348e7e3995d4
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/rules/field_mask.py
@@ -0,0 +1,36 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from google.protobuf import field_mask_pb2
+
+
+class FieldMaskRule:
+ """A marshal between FieldMask and strings.
+
+ See https://github.com/googleapis/proto-plus-python/issues/333
+ and
+ https://developers.google.com/protocol-buffers/docs/proto3#json
+ for more details.
+ """
+
+ def to_python(self, value, *, absent: bool = None):
+ return value
+
+ def to_proto(self, value):
+ if isinstance(value, str):
+ field_mask_value = field_mask_pb2.FieldMask()
+ field_mask_value.FromJsonString(value=value)
+ return field_mask_value
+
+ return value
diff --git a/packages/proto-plus/proto/marshal/rules/message.py b/packages/proto-plus/proto/marshal/rules/message.py
new file mode 100644
index 000000000000..c191180102fa
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/rules/message.py
@@ -0,0 +1,53 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class MessageRule:
+ """A marshal for converting between a descriptor and proto.Message."""
+
+ def __init__(self, descriptor: type, wrapper: type):
+ self._descriptor = descriptor
+ self._wrapper = wrapper
+
+ def to_python(self, value, *, absent: bool = None):
+ if isinstance(value, self._descriptor):
+ return self._wrapper.wrap(value)
+ return value
+
+ def to_proto(self, value):
+ if isinstance(value, self._wrapper):
+ return self._wrapper.pb(value)
+ if isinstance(value, dict) and not self.is_map:
+ # We need to use the wrapper's marshaling to handle
+ # potentially problematic nested messages.
+ try:
+ # Try the fast path first.
+ return self._descriptor(**value)
+ except (TypeError, ValueError, AttributeError) as ex:
+ # If we have a TypeError, ValueError or AttributeError,
+ # try the slow path in case the error
+ # was:
+ # - an int64/string issue.
+ # - a missing key issue in case a key only exists with a `_` suffix.
+ # See related issue: https://github.com/googleapis/python-api-core/issues/227.
+ # - a missing key issue due to nested struct. See: https://github.com/googleapis/proto-plus-python/issues/424.
+ # - a missing key issue due to nested duration. See: https://github.com/googleapis/google-cloud-python/issues/13350.
+ return self._wrapper(value)._pb
+ return value
+
+ @property
+ def is_map(self):
+ """Return True if the descriptor is a map entry, False otherwise."""
+ desc = self._descriptor.DESCRIPTOR
+ return desc.has_options and desc.GetOptions().map_entry
diff --git a/packages/proto-plus/proto/marshal/rules/stringy_numbers.py b/packages/proto-plus/proto/marshal/rules/stringy_numbers.py
new file mode 100644
index 000000000000..dae69e9c9121
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/rules/stringy_numbers.py
@@ -0,0 +1,71 @@
+# Copyright (C) 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from proto.primitives import ProtoType
+
+
+class StringyNumberRule:
+ """A marshal between certain numeric types and strings
+
+ This is a necessary hack to allow round trip conversion
+ from messages to dicts back to messages.
+
+ See https://github.com/protocolbuffers/protobuf/issues/2679
+ and
+ https://developers.google.com/protocol-buffers/docs/proto3#json
+ for more details.
+ """
+
+ def to_python(self, value, *, absent: bool = None):
+ return value
+
+ def to_proto(self, value):
+ if value is not None:
+ return self._python_type(value)
+
+ return None
+
+
+class Int64Rule(StringyNumberRule):
+ _python_type = int
+ _proto_type = ProtoType.INT64
+
+
+class UInt64Rule(StringyNumberRule):
+ _python_type = int
+ _proto_type = ProtoType.UINT64
+
+
+class SInt64Rule(StringyNumberRule):
+ _python_type = int
+ _proto_type = ProtoType.SINT64
+
+
+class Fixed64Rule(StringyNumberRule):
+ _python_type = int
+ _proto_type = ProtoType.FIXED64
+
+
+class SFixed64Rule(StringyNumberRule):
+ _python_type = int
+ _proto_type = ProtoType.SFIXED64
+
+
+STRINGY_NUMBER_RULES = [
+ Int64Rule,
+ UInt64Rule,
+ SInt64Rule,
+ Fixed64Rule,
+ SFixed64Rule,
+]
diff --git a/packages/proto-plus/proto/marshal/rules/struct.py b/packages/proto-plus/proto/marshal/rules/struct.py
new file mode 100644
index 000000000000..0e34587b26b1
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/rules/struct.py
@@ -0,0 +1,143 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import collections.abc
+
+from google.protobuf import struct_pb2
+
+from proto.marshal.collections import maps
+from proto.marshal.collections import repeated
+
+
+class ValueRule:
+ """A rule to marshal between google.protobuf.Value and Python values."""
+
+ def __init__(self, *, marshal):
+ self._marshal = marshal
+
+ def to_python(self, value, *, absent: bool = None):
+ """Coerce the given value to the appropriate Python type.
+
+ Note that both NullValue and absent fields return None.
+ In order to disambiguate between these two options,
+ use containment check,
+ E.g.
+ "value" in foo
+ which is True for NullValue and False for an absent value.
+ """
+ kind = value.WhichOneof("kind")
+ if kind == "null_value" or absent:
+ return None
+ if kind == "bool_value":
+ return bool(value.bool_value)
+ if kind == "number_value":
+ return float(value.number_value)
+ if kind == "string_value":
+ return str(value.string_value)
+ if kind == "struct_value":
+ return self._marshal.to_python(
+ struct_pb2.Struct,
+ value.struct_value,
+ absent=False,
+ )
+ if kind == "list_value":
+ return self._marshal.to_python(
+ struct_pb2.ListValue,
+ value.list_value,
+ absent=False,
+ )
+ # If more variants are ever added, we want to fail loudly
+ # instead of tacitly returning None.
+ raise ValueError("Unexpected kind: %s" % kind) # pragma: NO COVER
+
+ def to_proto(self, value) -> struct_pb2.Value:
+ """Return a protobuf Value object representing this value."""
+ if isinstance(value, struct_pb2.Value):
+ return value
+ if value is None:
+ return struct_pb2.Value(null_value=0)
+ if isinstance(value, bool):
+ return struct_pb2.Value(bool_value=value)
+ if isinstance(value, (int, float)):
+ return struct_pb2.Value(number_value=float(value))
+ if isinstance(value, str):
+ return struct_pb2.Value(string_value=value)
+ if isinstance(value, collections.abc.Sequence):
+ return struct_pb2.Value(
+ list_value=self._marshal.to_proto(struct_pb2.ListValue, value),
+ )
+ if isinstance(value, collections.abc.Mapping):
+ return struct_pb2.Value(
+ struct_value=self._marshal.to_proto(struct_pb2.Struct, value),
+ )
+ raise ValueError("Unable to coerce value: %r" % value)
+
+
+class ListValueRule:
+ """A rule translating google.protobuf.ListValue and list-like objects."""
+
+ def __init__(self, *, marshal):
+ self._marshal = marshal
+
+ def to_python(self, value, *, absent: bool = None):
+ """Coerce the given value to a Python sequence."""
+ return (
+ None
+ if absent
+ else repeated.RepeatedComposite(value.values, marshal=self._marshal)
+ )
+
+ def to_proto(self, value) -> struct_pb2.ListValue:
+ # We got a proto, or else something we sent originally.
+ # Preserve the instance we have.
+ if isinstance(value, struct_pb2.ListValue):
+ return value
+ if isinstance(value, repeated.RepeatedComposite):
+ return struct_pb2.ListValue(values=[v for v in value.pb])
+
+ # We got a list (or something list-like); convert it.
+ return struct_pb2.ListValue(
+ values=[self._marshal.to_proto(struct_pb2.Value, v) for v in value]
+ )
+
+
+class StructRule:
+ """A rule translating google.protobuf.Struct and dict-like objects."""
+
+ def __init__(self, *, marshal):
+ self._marshal = marshal
+
+ def to_python(self, value, *, absent: bool = None):
+ """Coerce the given value to a Python mapping."""
+ return (
+ None if absent else maps.MapComposite(value.fields, marshal=self._marshal)
+ )
+
+ def to_proto(self, value) -> struct_pb2.Struct:
+ # We got a proto, or else something we sent originally.
+ # Preserve the instance we have.
+ if isinstance(value, struct_pb2.Struct):
+ return value
+ if isinstance(value, maps.MapComposite):
+ return struct_pb2.Struct(
+ fields={k: v for k, v in value.pb.items()},
+ )
+
+ # We got a dict (or something dict-like); convert it.
+ answer = struct_pb2.Struct(
+ fields={
+ k: self._marshal.to_proto(struct_pb2.Value, v) for k, v in value.items()
+ }
+ )
+ return answer
diff --git a/packages/proto-plus/proto/marshal/rules/wrappers.py b/packages/proto-plus/proto/marshal/rules/wrappers.py
new file mode 100644
index 000000000000..5bc89e595851
--- /dev/null
+++ b/packages/proto-plus/proto/marshal/rules/wrappers.py
@@ -0,0 +1,84 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from google.protobuf import wrappers_pb2
+
+
+class WrapperRule:
+ """A marshal for converting the protobuf wrapper classes to Python.
+
+ This class converts between ``google.protobuf.BoolValue``,
+ ``google.protobuf.StringValue``, and their siblings to the appropriate
+ Python equivalents.
+
+ These are effectively similar to the protobuf primitives except
+ that None becomes a possible value.
+ """
+
+ def to_python(self, value, *, absent: bool = None):
+ if isinstance(value, self._proto_type):
+ if absent:
+ return None
+ return value.value
+ return value
+
+ def to_proto(self, value):
+ if isinstance(value, self._python_type):
+ return self._proto_type(value=value)
+ return value
+
+
+class DoubleValueRule(WrapperRule):
+ _proto_type = wrappers_pb2.DoubleValue
+ _python_type = float
+
+
+class FloatValueRule(WrapperRule):
+ _proto_type = wrappers_pb2.FloatValue
+ _python_type = float
+
+
+class Int64ValueRule(WrapperRule):
+ _proto_type = wrappers_pb2.Int64Value
+ _python_type = int
+
+
+class UInt64ValueRule(WrapperRule):
+ _proto_type = wrappers_pb2.UInt64Value
+ _python_type = int
+
+
+class Int32ValueRule(WrapperRule):
+ _proto_type = wrappers_pb2.Int32Value
+ _python_type = int
+
+
+class UInt32ValueRule(WrapperRule):
+ _proto_type = wrappers_pb2.UInt32Value
+ _python_type = int
+
+
+class BoolValueRule(WrapperRule):
+ _proto_type = wrappers_pb2.BoolValue
+ _python_type = bool
+
+
+class StringValueRule(WrapperRule):
+ _proto_type = wrappers_pb2.StringValue
+ _python_type = str
+
+
+class BytesValueRule(WrapperRule):
+ _proto_type = wrappers_pb2.BytesValue
+ _python_type = bytes
diff --git a/packages/proto-plus/proto/message.py b/packages/proto-plus/proto/message.py
new file mode 100644
index 000000000000..10a6f42d7612
--- /dev/null
+++ b/packages/proto-plus/proto/message.py
@@ -0,0 +1,969 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import collections
+import collections.abc
+import copy
+import re
+from typing import Any, Dict, List, Optional, Type
+import warnings
+
+import google.protobuf
+from google.protobuf import descriptor_pb2
+from google.protobuf import message
+from google.protobuf.json_format import MessageToDict, MessageToJson, Parse
+
+from proto import _file_info
+from proto import _package_info
+from proto.fields import Field
+from proto.fields import MapField
+from proto.fields import RepeatedField
+from proto.marshal import Marshal
+from proto.primitives import ProtoType
+from proto.utils import has_upb
+
+
+PROTOBUF_VERSION = google.protobuf.__version__
+
+_upb = has_upb() # Important to cache result here.
+
+
+class MessageMeta(type):
+ """A metaclass for building and registering Message subclasses."""
+
+ def __new__(mcls, name, bases, attrs):
+ # Do not do any special behavior for Message itself.
+ if not bases:
+ return super().__new__(mcls, name, bases, attrs)
+
+ # Get the essential information about the proto package, and where
+ # this component belongs within the file.
+ package, marshal = _package_info.compile(name, attrs)
+
+ # Determine the local path of this proto component within the file.
+ local_path = tuple(attrs.get("__qualname__", name).split("."))
+
+ # Sanity check: We get the wrong full name if a class is declared
+ # inside a function local scope; correct this.
+ if "" in local_path:
+ ix = local_path.index("")
+ local_path = local_path[: ix - 1] + local_path[ix + 1 :]
+
+ # Determine the full name in protocol buffers.
+ full_name = ".".join((package,) + local_path).lstrip(".")
+
+ # Special case: Maps. Map fields are special; they are essentially
+ # shorthand for a nested message and a repeated field of that message.
+ # Decompose each map into its constituent form.
+ # https://developers.google.com/protocol-buffers/docs/proto3#maps
+ map_fields = {}
+ for key, field in attrs.items():
+ if not isinstance(field, MapField):
+ continue
+
+ # Determine the name of the entry message.
+ msg_name = "{pascal_key}Entry".format(
+ pascal_key=re.sub(
+ r"_\w",
+ lambda m: m.group()[1:].upper(),
+ key,
+ ).replace(key[0], key[0].upper(), 1),
+ )
+
+ # Create the "entry" message (with the key and value fields).
+ #
+ # Note: We instantiate an ordered dictionary here and then
+ # attach key and value in order to ensure that the fields are
+ # iterated in the correct order when the class is created.
+ # This is only an issue in Python 3.5, where the order is
+ # random (and the wrong order causes the pool to refuse to add
+ # the descriptor because reasons).
+ entry_attrs = collections.OrderedDict(
+ {
+ "__module__": attrs.get("__module__", None),
+ "__qualname__": "{prefix}.{name}".format(
+ prefix=attrs.get("__qualname__", name),
+ name=msg_name,
+ ),
+ "_pb_options": {"map_entry": True},
+ }
+ )
+ entry_attrs["key"] = Field(field.map_key_type, number=1)
+ entry_attrs["value"] = Field(
+ field.proto_type,
+ number=2,
+ enum=field.enum,
+ message=field.message,
+ )
+ map_fields[msg_name] = MessageMeta(msg_name, (Message,), entry_attrs)
+
+ # Create the repeated field for the entry message.
+ map_fields[key] = RepeatedField(
+ ProtoType.MESSAGE,
+ number=field.number,
+ message=map_fields[msg_name],
+ )
+
+ # Add the new entries to the attrs
+ attrs.update(map_fields)
+
+ # Okay, now we deal with all the rest of the fields.
+ # Iterate over all the attributes and separate the fields into
+ # their own sequence.
+ fields = []
+ new_attrs = {}
+ oneofs = collections.OrderedDict()
+ proto_imports = set()
+ index = 0
+ for key, field in attrs.items():
+ # Sanity check: If this is not a field, do nothing.
+ if not isinstance(field, Field):
+ # The field objects themselves should not be direct attributes.
+ new_attrs[key] = field
+ continue
+
+ # Add data that the field requires that we do not take in the
+ # constructor because we can derive it from the metaclass.
+ # (The goal is to make the declaration syntax as nice as possible.)
+ field.mcls_data = {
+ "name": key,
+ "parent_name": full_name,
+ "index": index,
+ "package": package,
+ }
+
+ # Add the field to the list of fields.
+ fields.append(field)
+ # If this field is part of a "oneof", ensure the oneof itself
+ # is represented.
+ if field.oneof:
+ # Keep a running tally of the index of each oneof, and assign
+ # that index to the field's descriptor.
+ oneofs.setdefault(field.oneof, len(oneofs))
+ field.descriptor.oneof_index = oneofs[field.oneof]
+
+ # If this field references a message, it may be from another
+ # proto file; ensure we know about the import (to faithfully
+ # construct our file descriptor proto).
+ if field.message and not isinstance(field.message, str):
+ field_msg = field.message
+ if hasattr(field_msg, "pb") and callable(field_msg.pb):
+ field_msg = field_msg.pb()
+ # Sanity check: The field's message may not yet be defined if
+ # it was a Message defined in the same file, and the file
+ # descriptor proto has not yet been generated.
+ #
+ # We do nothing in this situation; everything will be handled
+ # correctly when the file descriptor is created later.
+ if field_msg:
+ proto_imports.add(field_msg.DESCRIPTOR.file.name)
+
+ # Same thing, but for enums.
+ elif field.enum and not isinstance(field.enum, str):
+ field_enum = (
+ field.enum._meta.pb
+ if hasattr(field.enum, "_meta")
+ else field.enum.DESCRIPTOR
+ )
+
+ if field_enum:
+ proto_imports.add(field_enum.file.name)
+
+ # Increment the field index counter.
+ index += 1
+
+ # As per descriptor.proto, all synthetic oneofs must be ordered after
+ # 'real' oneofs.
+ opt_attrs = {}
+ for field in fields:
+ if field.optional:
+ field.oneof = "_{}".format(field.name)
+ field.descriptor.oneof_index = oneofs[field.oneof] = len(oneofs)
+ opt_attrs[field.name] = field.name
+
+ # Generating a metaclass dynamically provides class attributes that
+ # instances can't see. This provides idiomatically named constants
+ # that enable the following pattern to check for field presence:
+ #
+ # class MyMessage(proto.Message):
+ # field = proto.Field(proto.INT32, number=1, optional=True)
+ #
+ # m = MyMessage()
+ # MyMessage.field in m
+ if opt_attrs:
+ mcls = type("AttrsMeta", (mcls,), opt_attrs)
+
+ # Determine the filename.
+ # We determine an appropriate proto filename based on the
+ # Python module.
+ filename = _file_info._FileInfo.proto_file_name(
+ new_attrs.get("__module__", name.lower())
+ )
+
+ # Get or create the information about the file, including the
+ # descriptor to which the new message descriptor shall be added.
+ file_info = _file_info._FileInfo.maybe_add_descriptor(filename, package)
+
+ # Ensure any imports that would be necessary are assigned to the file
+ # descriptor proto being created.
+ for proto_import in proto_imports:
+ if proto_import not in file_info.descriptor.dependency:
+ file_info.descriptor.dependency.append(proto_import)
+
+ # Retrieve any message options.
+ opts = descriptor_pb2.MessageOptions(**new_attrs.pop("_pb_options", {}))
+
+ # Create the underlying proto descriptor.
+ desc = descriptor_pb2.DescriptorProto(
+ name=name,
+ field=[i.descriptor for i in fields],
+ oneof_decl=[
+ descriptor_pb2.OneofDescriptorProto(name=i) for i in oneofs.keys()
+ ],
+ options=opts,
+ )
+
+ # If any descriptors were nested under this one, they need to be
+ # attached as nested types here.
+ child_paths = [p for p in file_info.nested.keys() if local_path == p[:-1]]
+ for child_path in child_paths:
+ desc.nested_type.add().MergeFrom(file_info.nested.pop(child_path))
+
+ # Same thing, but for enums
+ child_paths = [p for p in file_info.nested_enum.keys() if local_path == p[:-1]]
+ for child_path in child_paths:
+ desc.enum_type.add().MergeFrom(file_info.nested_enum.pop(child_path))
+
+ # Add the descriptor to the file if it is a top-level descriptor,
+ # or to a "holding area" for nested messages otherwise.
+ if len(local_path) == 1:
+ file_info.descriptor.message_type.add().MergeFrom(desc)
+ else:
+ file_info.nested[local_path] = desc
+
+ # Create the MessageInfo instance to be attached to this message.
+ new_attrs["_meta"] = _MessageInfo(
+ fields=fields,
+ full_name=full_name,
+ marshal=marshal,
+ options=opts,
+ package=package,
+ )
+
+ # Run the superclass constructor.
+ cls = super().__new__(mcls, name, bases, new_attrs)
+
+ # The info class and fields need a reference to the class just created.
+ cls._meta.parent = cls
+ for field in cls._meta.fields.values():
+ field.parent = cls
+
+ # Add this message to the _FileInfo instance; this allows us to
+ # associate the descriptor with the message once the descriptor
+ # is generated.
+ file_info.messages[full_name] = cls
+
+ # Generate the descriptor for the file if it is ready.
+ if file_info.ready(new_class=cls):
+ file_info.generate_file_pb(new_class=cls, fallback_salt=full_name)
+
+ # Done; return the class.
+ return cls
+
+ @classmethod
+ def __prepare__(mcls, name, bases, **kwargs):
+ return collections.OrderedDict()
+
+ @property
+ def meta(cls):
+ return cls._meta
+
+ def __dir__(self):
+ try:
+ names = set(dir(type))
+ names.update(
+ (
+ "meta",
+ "pb",
+ "wrap",
+ "serialize",
+ "deserialize",
+ "to_json",
+ "from_json",
+ "to_dict",
+ "copy_from",
+ )
+ )
+ desc = self.pb().DESCRIPTOR
+ names.update(t.name for t in desc.nested_types)
+ names.update(e.name for e in desc.enum_types)
+
+ return names
+ except AttributeError:
+ return dir(type)
+
+ def pb(cls, obj=None, *, coerce: bool = False):
+ """Return the underlying protobuf Message class or instance.
+
+ Args:
+ obj: If provided, and an instance of ``cls``, return the
+ underlying protobuf instance.
+ coerce (bool): If provided, will attempt to coerce ``obj`` to
+ ``cls`` if it is not already an instance.
+ """
+ if obj is None:
+ return cls.meta.pb
+ if not isinstance(obj, cls):
+ if coerce:
+ obj = cls(obj)
+ else:
+ raise TypeError(
+ "%r is not an instance of %s"
+ % (
+ obj,
+ cls.__name__,
+ )
+ )
+ return obj._pb
+
+ def wrap(cls, pb):
+ """Return a Message object that shallowly wraps the descriptor.
+
+ Args:
+ pb: A protocol buffer object, such as would be returned by
+ :meth:`pb`.
+ """
+ # Optimized fast path.
+ instance = cls.__new__(cls)
+ super(cls, instance).__setattr__("_pb", pb)
+ return instance
+
+ def serialize(cls, instance) -> bytes:
+ """Return the serialized proto.
+
+ Args:
+ instance: An instance of this message type, or something
+ compatible (accepted by the type's constructor).
+
+ Returns:
+ bytes: The serialized representation of the protocol buffer.
+ """
+ return cls.pb(instance, coerce=True).SerializeToString()
+
+ def deserialize(cls, payload: bytes) -> "Message":
+ """Given a serialized proto, deserialize it into a Message instance.
+
+ Args:
+ payload (bytes): The serialized proto.
+
+ Returns:
+ ~.Message: An instance of the message class against which this
+ method was called.
+ """
+ return cls.wrap(cls.pb().FromString(payload))
+
+ def _warn_if_including_default_value_fields_is_used_protobuf_5(
+ cls, including_default_value_fields: Optional[bool]
+ ) -> None:
+ """
+ Warn Protobuf 5.x+ users that `including_default_value_fields` is deprecated if it is set.
+
+ Args:
+ including_default_value_fields (Optional(bool)): The value of `including_default_value_fields` set by the user.
+ """
+ if (
+ PROTOBUF_VERSION[0] not in ("3", "4")
+ and including_default_value_fields is not None
+ ):
+ warnings.warn(
+ """The argument `including_default_value_fields` has been removed from
+ Protobuf 5.x. Please use `always_print_fields_with_no_presence` instead.
+ """,
+ DeprecationWarning,
+ )
+
+ def _raise_if_print_fields_values_are_set_and_differ(
+ cls,
+ always_print_fields_with_no_presence: Optional[bool],
+ including_default_value_fields: Optional[bool],
+ ) -> None:
+ """
+ Raise Exception if both `always_print_fields_with_no_presence` and `including_default_value_fields` are set
+ and the values differ.
+
+ Args:
+ always_print_fields_with_no_presence (Optional(bool)): The value of `always_print_fields_with_no_presence` set by the user.
+ including_default_value_fields (Optional(bool)): The value of `including_default_value_fields` set by the user.
+ Returns:
+ None
+ Raises:
+ ValueError: if both `always_print_fields_with_no_presence` and `including_default_value_fields` are set and
+ the values differ.
+ """
+ if (
+ always_print_fields_with_no_presence is not None
+ and including_default_value_fields is not None
+ and always_print_fields_with_no_presence != including_default_value_fields
+ ):
+ raise ValueError(
+ "Arguments `always_print_fields_with_no_presence` and `including_default_value_fields` must match"
+ )
+
+ def _normalize_print_fields_without_presence(
+ cls,
+ always_print_fields_with_no_presence: Optional[bool],
+ including_default_value_fields: Optional[bool],
+ ) -> bool:
+ """
+ Return true if fields with no presence should be included in the results.
+ By default, fields with no presence will be included in the results
+ when both `always_print_fields_with_no_presence` and
+ `including_default_value_fields` are not set
+
+ Args:
+ always_print_fields_with_no_presence (Optional(bool)): The value of `always_print_fields_with_no_presence` set by the user.
+ including_default_value_fields (Optional(bool)): The value of `including_default_value_fields` set by the user.
+ Returns:
+ None
+ Raises:
+ ValueError: if both `always_print_fields_with_no_presence` and `including_default_value_fields` are set and
+ the values differ.
+ """
+
+ cls._warn_if_including_default_value_fields_is_used_protobuf_5(
+ including_default_value_fields
+ )
+ cls._raise_if_print_fields_values_are_set_and_differ(
+ always_print_fields_with_no_presence, including_default_value_fields
+ )
+ # Default to True if neither `always_print_fields_with_no_presence` or `including_default_value_fields` is set
+ return (
+ (
+ always_print_fields_with_no_presence is None
+ and including_default_value_fields is None
+ )
+ or always_print_fields_with_no_presence
+ or including_default_value_fields
+ )
+
+ def to_json(
+ cls,
+ instance,
+ *,
+ use_integers_for_enums=True,
+ including_default_value_fields=None,
+ preserving_proto_field_name=False,
+ sort_keys=False,
+ indent=2,
+ float_precision=None,
+ always_print_fields_with_no_presence=None,
+ ) -> str:
+ """Given a message instance, serialize it to json
+
+ Args:
+ instance: An instance of this message type, or something
+ compatible (accepted by the type's constructor).
+ use_integers_for_enums (Optional(bool)): An option that determines whether enum
+ values should be represented by strings (False) or integers (True).
+ Default is True.
+ including_default_value_fields (Optional(bool)): Deprecated. Use argument
+ `always_print_fields_with_no_presence` instead. An option that
+ determines whether the default field values should be included in the results.
+ This value must match `always_print_fields_with_no_presence`,
+ if both arguments are explicitly set.
+ preserving_proto_field_name (Optional(bool)): An option that
+ determines whether field name representations preserve
+ proto case (snake_case) or use lowerCamelCase. Default is False.
+ sort_keys (Optional(bool)): If True, then the output will be sorted by field names.
+ Default is False.
+ indent (Optional(int)): The JSON object will be pretty-printed with this indent level.
+ An indent level of 0 or negative will only insert newlines.
+ Pass None for the most compact representation without newlines.
+ float_precision (Optional(int)): If set, use this to specify float field valid digits.
+ Default is None.
+ always_print_fields_with_no_presence (Optional(bool)): If True, fields without
+ presence (implicit presence scalars, repeated fields, and map fields) will
+ always be serialized. Any field that supports presence is not affected by
+ this option (including singular message fields and oneof fields).
+ This value must match `including_default_value_fields`,
+ if both arguments are explicitly set.
+ Returns:
+ str: The json string representation of the protocol buffer.
+ """
+
+ print_fields = cls._normalize_print_fields_without_presence(
+ always_print_fields_with_no_presence, including_default_value_fields
+ )
+
+ if PROTOBUF_VERSION[0] in ("3", "4"):
+ return MessageToJson(
+ cls.pb(instance),
+ use_integers_for_enums=use_integers_for_enums,
+ including_default_value_fields=print_fields,
+ preserving_proto_field_name=preserving_proto_field_name,
+ sort_keys=sort_keys,
+ indent=indent,
+ float_precision=float_precision,
+ )
+ else:
+ # The `including_default_value_fields` argument was removed from protobuf 5.x
+ # and replaced with `always_print_fields_with_no_presence` which very similar but has
+ # handles optional fields consistently by not affecting them.
+ # The old flag accidentally had inconsistent behavior between proto2
+ # optional and proto3 optional fields.
+ return MessageToJson(
+ cls.pb(instance),
+ use_integers_for_enums=use_integers_for_enums,
+ always_print_fields_with_no_presence=print_fields,
+ preserving_proto_field_name=preserving_proto_field_name,
+ sort_keys=sort_keys,
+ indent=indent,
+ float_precision=float_precision,
+ )
+
+ def from_json(cls, payload, *, ignore_unknown_fields=False) -> "Message":
+ """Given a json string representing an instance,
+ parse it into a message.
+
+ Args:
+ payload: A json string representing a message.
+ ignore_unknown_fields (Optional(bool)): If True, do not raise errors
+ for unknown fields.
+
+ Returns:
+ ~.Message: An instance of the message class against which this
+ method was called.
+ """
+ instance = cls()
+ Parse(payload, instance._pb, ignore_unknown_fields=ignore_unknown_fields)
+ return instance
+
+ def to_dict(
+ cls,
+ instance,
+ *,
+ use_integers_for_enums=True,
+ preserving_proto_field_name=True,
+ including_default_value_fields=None,
+ float_precision=None,
+ always_print_fields_with_no_presence=None,
+ ) -> Dict[str, Any]:
+ """Given a message instance, return its representation as a python dict.
+
+ Args:
+ instance: An instance of this message type, or something
+ compatible (accepted by the type's constructor).
+ use_integers_for_enums (Optional(bool)): An option that determines whether enum
+ values should be represented by strings (False) or integers (True).
+ Default is True.
+ preserving_proto_field_name (Optional(bool)): An option that
+ determines whether field name representations preserve
+ proto case (snake_case) or use lowerCamelCase. Default is True.
+ including_default_value_fields (Optional(bool)): Deprecated. Use argument
+ `always_print_fields_with_no_presence` instead. An option that
+ determines whether the default field values should be included in the results.
+ This value must match `always_print_fields_with_no_presence`,
+ if both arguments are explicitly set.
+ float_precision (Optional(int)): If set, use this to specify float field valid digits.
+ Default is None.
+ always_print_fields_with_no_presence (Optional(bool)): If True, fields without
+ presence (implicit presence scalars, repeated fields, and map fields) will
+ always be serialized. Any field that supports presence is not affected by
+ this option (including singular message fields and oneof fields). This value
+ must match `including_default_value_fields`, if both arguments are explicitly set.
+
+ Returns:
+ dict: A representation of the protocol buffer using pythonic data structures.
+ Messages and map fields are represented as dicts,
+ repeated fields are represented as lists.
+ """
+
+ print_fields = cls._normalize_print_fields_without_presence(
+ always_print_fields_with_no_presence, including_default_value_fields
+ )
+
+ if PROTOBUF_VERSION[0] in ("3", "4"):
+ return MessageToDict(
+ cls.pb(instance),
+ including_default_value_fields=print_fields,
+ preserving_proto_field_name=preserving_proto_field_name,
+ use_integers_for_enums=use_integers_for_enums,
+ float_precision=float_precision,
+ )
+ else:
+ # The `including_default_value_fields` argument was removed from protobuf 5.x
+ # and replaced with `always_print_fields_with_no_presence` which very similar but has
+ # handles optional fields consistently by not affecting them.
+ # The old flag accidentally had inconsistent behavior between proto2
+ # optional and proto3 optional fields.
+ return MessageToDict(
+ cls.pb(instance),
+ always_print_fields_with_no_presence=print_fields,
+ preserving_proto_field_name=preserving_proto_field_name,
+ use_integers_for_enums=use_integers_for_enums,
+ float_precision=float_precision,
+ )
+
+ def copy_from(cls, instance, other):
+ """Equivalent for protobuf.Message.CopyFrom
+
+ Args:
+ instance: An instance of this message type
+ other: (Union[dict, ~.Message):
+ A dictionary or message to reinitialize the values for this message.
+ """
+ if isinstance(other, cls):
+ # Just want the underlying proto.
+ other = Message.pb(other)
+ elif isinstance(other, cls.pb()):
+ # Don't need to do anything.
+ pass
+ elif isinstance(other, collections.abc.Mapping):
+ # Coerce into a proto
+ other = cls._meta.pb(**other)
+ else:
+ raise TypeError(
+ "invalid argument type to copy to {}: {}".format(
+ cls.__name__, other.__class__.__name__
+ )
+ )
+
+ # Note: we can't just run self.__init__ because this may be a message field
+ # for a higher order proto; the memory layout for protos is NOT LIKE the
+ # python memory model. We cannot rely on just setting things by reference.
+ # Non-trivial complexity is (partially) hidden by the protobuf runtime.
+ cls.pb(instance).CopyFrom(other)
+
+
+class Message(metaclass=MessageMeta):
+ """The abstract base class for a message.
+
+ Args:
+ mapping (Union[dict, ~.Message]): A dictionary or message to be
+ used to determine the values for this message.
+ ignore_unknown_fields (Optional(bool)): If True, do not raise errors for
+ unknown fields. Only applied if `mapping` is a mapping type or there
+ are keyword parameters.
+ kwargs (dict): Keys and values corresponding to the fields of the
+ message.
+ """
+
+ def __init__(
+ self,
+ mapping=None,
+ *,
+ ignore_unknown_fields=False,
+ **kwargs,
+ ):
+ # We accept several things for `mapping`:
+ # * An instance of this class.
+ # * An instance of the underlying protobuf descriptor class.
+ # * A dict
+ # * Nothing (keyword arguments only).
+ if mapping is None:
+ if not kwargs:
+ # Special fast path for empty construction.
+ super().__setattr__("_pb", self._meta.pb())
+ return
+
+ mapping = kwargs
+ elif isinstance(mapping, self._meta.pb):
+ # Make a copy of the mapping.
+ # This is a constructor for a new object, so users will assume
+ # that it will not have side effects on the arguments being
+ # passed in.
+ #
+ # The `wrap` method on the metaclass is the public API for taking
+ # ownership of the passed in protobuf object.
+ mapping = copy.deepcopy(mapping)
+ if kwargs:
+ mapping.MergeFrom(self._meta.pb(**kwargs))
+
+ super().__setattr__("_pb", mapping)
+ return
+ elif isinstance(mapping, type(self)):
+ # Just use the above logic on mapping's underlying pb.
+ self.__init__(mapping=mapping._pb, **kwargs)
+ return
+ elif isinstance(mapping, collections.abc.Mapping):
+ # Can't have side effects on mapping.
+ mapping = copy.copy(mapping)
+ # kwargs entries take priority for duplicate keys.
+ mapping.update(kwargs)
+ else:
+ # Sanity check: Did we get something not a map? Error if so.
+ raise TypeError(
+ "Invalid constructor input for %s: %r"
+ % (
+ self.__class__.__name__,
+ mapping,
+ )
+ )
+
+ params = {}
+ # Update the mapping to address any values that need to be
+ # coerced.
+ marshal = self._meta.marshal
+ for key, value in mapping.items():
+ (key, pb_type) = self._get_pb_type_from_key(key)
+ if pb_type is None:
+ if ignore_unknown_fields:
+ continue
+
+ raise ValueError(
+ "Unknown field for {}: {}".format(self.__class__.__name__, key)
+ )
+
+ pb_value = marshal.to_proto(pb_type, value)
+
+ if pb_value is not None:
+ params[key] = pb_value
+
+ # Create the internal protocol buffer.
+ super().__setattr__("_pb", self._meta.pb(**params))
+
+ def _get_pb_type_from_key(self, key):
+ """Given a key, return the corresponding pb_type.
+
+ Args:
+ key(str): The name of the field.
+
+ Returns:
+ A tuple containing a key and pb_type. The pb_type will be
+ the composite type of the field, or the primitive type if a primitive.
+ If no corresponding field exists, return None.
+ """
+
+ pb_type = None
+
+ try:
+ pb_type = self._meta.fields[key].pb_type
+ except KeyError:
+ # Underscores may be appended to field names
+ # that collide with python or proto-plus keywords.
+ # In case a key only exists with a `_` suffix, coerce the key
+ # to include the `_` suffix. It's not possible to
+ # natively define the same field with a trailing underscore in protobuf.
+ # See related issue
+ # https://github.com/googleapis/python-api-core/issues/227
+ if f"{key}_" in self._meta.fields:
+ key = f"{key}_"
+ pb_type = self._meta.fields[key].pb_type
+
+ return (key, pb_type)
+
+ def __dir__(self):
+ desc = type(self).pb().DESCRIPTOR
+ names = {f_name for f_name in self._meta.fields.keys()}
+ names.update(m.name for m in desc.nested_types)
+ names.update(e.name for e in desc.enum_types)
+ names.update(dir(object()))
+ # Can't think of a better way of determining
+ # the special methods than manually listing them.
+ names.update(
+ (
+ "__bool__",
+ "__contains__",
+ "__dict__",
+ "__getattr__",
+ "__getstate__",
+ "__module__",
+ "__setstate__",
+ "__weakref__",
+ )
+ )
+
+ return names
+
+ def __bool__(self):
+ """Return True if any field is truthy, False otherwise."""
+ return any(k in self and getattr(self, k) for k in self._meta.fields.keys())
+
+ def __contains__(self, key):
+ """Return True if this field was set to something non-zero on the wire.
+
+ In most cases, this method will return True when ``__getattr__``
+ would return a truthy value and False when it would return a falsy
+ value, so explicitly calling this is not useful.
+
+ The exception case is empty messages explicitly set on the wire,
+ which are falsy from ``__getattr__``. This method allows to
+ distinguish between an explicitly provided empty message and the
+ absence of that message, which is useful in some edge cases.
+
+ The most common edge case is the use of ``google.protobuf.BoolValue``
+ to get a boolean that distinguishes between ``False`` and ``None``
+ (or the same for a string, int, etc.). This library transparently
+ handles that case for you, but this method remains available to
+ accommodate cases not automatically covered.
+
+ Args:
+ key (str): The name of the field.
+
+ Returns:
+ bool: Whether the field's value corresponds to a non-empty
+ wire serialization.
+ """
+ pb_value = getattr(self._pb, key)
+ try:
+ # Protocol buffers "HasField" is unfriendly; it only works
+ # against composite, non-repeated fields, and raises ValueError
+ # against any repeated field or primitive.
+ #
+ # There is no good way to test whether it is valid to provide
+ # a field to this method, so sadly we are stuck with a
+ # somewhat inefficient try/except.
+ return self._pb.HasField(key)
+ except ValueError:
+ return bool(pb_value)
+
+ def __delattr__(self, key):
+ """Delete the value on the given field.
+
+ This is generally equivalent to setting a falsy value.
+ """
+ self._pb.ClearField(key)
+
+ def __eq__(self, other):
+ """Return True if the messages are equal, False otherwise."""
+ # If these are the same type, use internal protobuf's equality check.
+ if isinstance(other, type(self)):
+ return self._pb == other._pb
+
+ # If the other type is the target protobuf object, honor that also.
+ if isinstance(other, self._meta.pb):
+ return self._pb == other
+
+ # Ask the other object.
+ return NotImplemented
+
+ def __getattr__(self, key):
+ """Retrieve the given field's value.
+
+ In protocol buffers, the presence of a field on a message is
+ sufficient for it to always be "present".
+
+ For primitives, a value of the correct type will always be returned
+ (the "falsy" values in protocol buffers consistently match those
+ in Python). For repeated fields, the falsy value is always an empty
+ sequence.
+
+ For messages, protocol buffers does distinguish between an empty
+ message and absence, but this distinction is subtle and rarely
+ relevant. Therefore, this method always returns an empty message
+ (following the official implementation). To check for message
+ presence, use ``key in self`` (in other words, ``__contains__``).
+
+ .. note::
+
+ Some well-known protocol buffer types
+ (e.g. ``google.protobuf.Timestamp``) will be converted to
+ their Python equivalents. See the ``marshal`` module for
+ more details.
+ """
+ (key, pb_type) = self._get_pb_type_from_key(key)
+ if pb_type is None:
+ raise AttributeError(
+ "Unknown field for {}: {}".format(self.__class__.__name__, key)
+ )
+ pb_value = getattr(self._pb, key)
+ marshal = self._meta.marshal
+ return marshal.to_python(pb_type, pb_value, absent=key not in self)
+
+ def __ne__(self, other):
+ """Return True if the messages are unequal, False otherwise."""
+ return not self == other
+
+ def __repr__(self):
+ return repr(self._pb)
+
+ def __setattr__(self, key, value):
+ """Set the value on the given field.
+
+ For well-known protocol buffer types which are marshalled, either
+ the protocol buffer object or the Python equivalent is accepted.
+ """
+ if key[0] == "_":
+ return super().__setattr__(key, value)
+ marshal = self._meta.marshal
+ (key, pb_type) = self._get_pb_type_from_key(key)
+ if pb_type is None:
+ raise AttributeError(
+ "Unknown field for {}: {}".format(self.__class__.__name__, key)
+ )
+
+ pb_value = marshal.to_proto(pb_type, value)
+
+ # Clear the existing field.
+ # This is the only way to successfully write nested falsy values,
+ # because otherwise MergeFrom will no-op on them.
+ self._pb.ClearField(key)
+
+ # Merge in the value being set.
+ if pb_value is not None:
+ self._pb.MergeFrom(self._meta.pb(**{key: pb_value}))
+
+ def __getstate__(self):
+ """Serialize for pickling."""
+ return self._pb.SerializeToString()
+
+ def __setstate__(self, value):
+ """Deserialization for pickling."""
+ new_pb = self._meta.pb().FromString(value)
+ super().__setattr__("_pb", new_pb)
+
+
+class _MessageInfo:
+ """Metadata about a message.
+
+ Args:
+ fields (Tuple[~.fields.Field]): The fields declared on the message.
+ package (str): The proto package.
+ full_name (str): The full name of the message.
+ file_info (~._FileInfo): The file descriptor and messages for the
+ file containing this message.
+ marshal (~.Marshal): The marshal instance to which this message was
+ automatically registered.
+ options (~.descriptor_pb2.MessageOptions): Any options that were
+ set on the message.
+ """
+
+ def __init__(
+ self,
+ *,
+ fields: List[Field],
+ package: str,
+ full_name: str,
+ marshal: Marshal,
+ options: descriptor_pb2.MessageOptions,
+ ) -> None:
+ self.package = package
+ self.full_name = full_name
+ self.options = options
+ self.fields = collections.OrderedDict((i.name, i) for i in fields)
+ self.fields_by_number = collections.OrderedDict((i.number, i) for i in fields)
+ self.marshal = marshal
+ self._pb = None
+
+ @property
+ def pb(self) -> Type[message.Message]:
+ """Return the protobuf message type for this descriptor.
+
+ If a field on the message references another message which has not
+ loaded, then this method returns None.
+ """
+ return self._pb
+
+
+__all__ = ("Message",)
diff --git a/packages/proto-plus/proto/modules.py b/packages/proto-plus/proto/modules.py
new file mode 100644
index 000000000000..45864a937c58
--- /dev/null
+++ b/packages/proto-plus/proto/modules.py
@@ -0,0 +1,50 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Set
+import collections
+
+
+_ProtoModule = collections.namedtuple(
+ "ProtoModule",
+ ["package", "marshal", "manifest"],
+)
+
+
+def define_module(
+ *, package: str, marshal: str = None, manifest: Set[str] = frozenset()
+) -> _ProtoModule:
+ """Define a protocol buffers module.
+
+ The settings defined here are used for all protobuf messages
+ declared in the module of the given name.
+
+ Args:
+ package (str): The proto package name.
+ marshal (str): The name of the marshal to use. It is recommended
+ to use one marshal per Python library (e.g. package on PyPI).
+ manifest (Set[str]): A set of messages and enums to be created. Setting
+ this adds a slight efficiency in piecing together proto
+ descriptors under the hood.
+ """
+ if not marshal:
+ marshal = package
+ return _ProtoModule(
+ package=package,
+ marshal=marshal,
+ manifest=frozenset(manifest),
+ )
+
+
+__all__ = ("define_module",)
diff --git a/packages/proto-plus/proto/primitives.py b/packages/proto-plus/proto/primitives.py
new file mode 100644
index 000000000000..cff2094c678b
--- /dev/null
+++ b/packages/proto-plus/proto/primitives.py
@@ -0,0 +1,38 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import enum
+
+
+class ProtoType(enum.IntEnum):
+ """The set of basic types in protocol buffers."""
+
+ # These values come from google/protobuf/descriptor.proto
+ DOUBLE = 1
+ FLOAT = 2
+ INT64 = 3
+ UINT64 = 4
+ INT32 = 5
+ FIXED64 = 6
+ FIXED32 = 7
+ BOOL = 8
+ STRING = 9
+ MESSAGE = 11
+ BYTES = 12
+ UINT32 = 13
+ ENUM = 14
+ SFIXED32 = 15
+ SFIXED64 = 16
+ SINT32 = 17
+ SINT64 = 18
diff --git a/packages/proto-plus/proto/utils.py b/packages/proto-plus/proto/utils.py
new file mode 100644
index 000000000000..ac3c471a2e8b
--- /dev/null
+++ b/packages/proto-plus/proto/utils.py
@@ -0,0 +1,58 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import functools
+
+
+def has_upb():
+ try:
+ from google._upb import _message # pylint: disable=unused-import
+
+ has_upb = True
+ except ImportError:
+ has_upb = False
+ return has_upb
+
+
+def cached_property(fx):
+ """Make the callable into a cached property.
+
+ Similar to @property, but the function will only be called once per
+ object.
+
+ Args:
+ fx (Callable[]): The property function.
+
+ Returns:
+ Callable[]: The wrapped function.
+ """
+
+ @functools.wraps(fx)
+ def inner(self):
+ # Sanity check: If there is no cache at all, create an empty cache.
+ if not hasattr(self, "_cached_values"):
+ object.__setattr__(self, "_cached_values", {})
+
+ # If and only if the function's result is not in the cache,
+ # run the function.
+ if fx.__name__ not in self._cached_values:
+ self._cached_values[fx.__name__] = fx(self)
+
+ # Return the value from cache.
+ return self._cached_values[fx.__name__]
+
+ return property(inner)
+
+
+__all__ = ("cached_property",)
diff --git a/packages/proto-plus/proto/version.py b/packages/proto-plus/proto/version.py
new file mode 100644
index 000000000000..46d6b35ed597
--- /dev/null
+++ b/packages/proto-plus/proto/version.py
@@ -0,0 +1,15 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+__version__ = "1.26.1"
diff --git a/packages/proto-plus/pyproject.toml b/packages/proto-plus/pyproject.toml
new file mode 100644
index 000000000000..3a031d6640da
--- /dev/null
+++ b/packages/proto-plus/pyproject.toml
@@ -0,0 +1,58 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "proto-plus"
+authors = [{ name = "Google LLC", email = "googleapis-packages@google.com" }]
+license = { text = "Apache 2.0" }
+requires-python = ">=3.7"
+description = "Beautiful, Pythonic protocol buffers"
+readme = "README.rst"
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Operating System :: OS Independent",
+ "Topic :: Software Development :: Code Generators",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+]
+dependencies = ["protobuf >=3.19.0, < 7.0.0"]
+dynamic = ["version"]
+
+[project.urls]
+Documentation = "https://googleapis.dev/python/proto-plus/latest/"
+Repository = "https://github.com/googleapis/proto-plus-python"
+
+[project.optional-dependencies]
+testing = ["google-api-core >= 1.31.5"]
+
+[tool.setuptools.dynamic]
+version = { attr = "proto.version.__version__" }
+
+[tool.setuptools.packages.find]
+# Only include packages under the 'proto' namespace. Do not include build, docs, tests
+include = ["proto*"]
diff --git a/packages/proto-plus/pytest.ini b/packages/proto-plus/pytest.ini
new file mode 100644
index 000000000000..4f8fe4d2a91b
--- /dev/null
+++ b/packages/proto-plus/pytest.ini
@@ -0,0 +1,11 @@
+[pytest]
+filterwarnings =
+ # treat all warnings as errors
+ error
+ # Remove once https://github.com/protocolbuffers/protobuf/issues/12186 is fixed
+ ignore:.*custom tp_new.*in Python 3.14:DeprecationWarning
+ # Remove once deprecated field `including_default_value_fields` is removed
+ ignore:.*The argument `including_default_value_fields` has been removed.*:DeprecationWarning
+ # Remove once deprecated field `float_precision` is removed
+ # See https://github.com/googleapis/proto-plus-python/issues/547
+ ignore:float_precision option is deprecated for json_format:UserWarning
diff --git a/packages/proto-plus/renovate.json b/packages/proto-plus/renovate.json
new file mode 100644
index 000000000000..a35fc4d36c4e
--- /dev/null
+++ b/packages/proto-plus/renovate.json
@@ -0,0 +1,11 @@
+{
+ "extends": [
+ "config:base",
+ "group:all",
+ ":disableDependencyDashboard",
+ "schedule:weekly"
+ ],
+ "ignorePaths": [
+ ".kokoro/requirements.txt"
+ ]
+}
diff --git a/packages/proto-plus/setup.py b/packages/proto-plus/setup.py
new file mode 100644
index 000000000000..12e9ed26dcdf
--- /dev/null
+++ b/packages/proto-plus/setup.py
@@ -0,0 +1,19 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from setuptools import setup
+
+
+setup()
diff --git a/packages/proto-plus/testing/constraints-3.10.txt b/packages/proto-plus/testing/constraints-3.10.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/proto-plus/testing/constraints-3.11.txt b/packages/proto-plus/testing/constraints-3.11.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/proto-plus/testing/constraints-3.12.txt b/packages/proto-plus/testing/constraints-3.12.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/proto-plus/testing/constraints-3.13.txt b/packages/proto-plus/testing/constraints-3.13.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/proto-plus/testing/constraints-3.14.txt b/packages/proto-plus/testing/constraints-3.14.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/proto-plus/testing/constraints-3.7.txt b/packages/proto-plus/testing/constraints-3.7.txt
new file mode 100644
index 000000000000..849c8e7cb244
--- /dev/null
+++ b/packages/proto-plus/testing/constraints-3.7.txt
@@ -0,0 +1,9 @@
+# This constraints file is used to check that lower bounds
+# are correct in setup.py
+# List *all* library dependencies and extras in this file.
+# Pin the version to the lower bound.
+#
+# e.g., if setup.py has "foo >= 1.14.0, < 2.0.0dev",
+# Then this file should have foo==1.14.0
+google-api-core==1.31.5
+protobuf==3.19.0
diff --git a/packages/proto-plus/testing/constraints-3.8.txt b/packages/proto-plus/testing/constraints-3.8.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/proto-plus/testing/constraints-3.9.txt b/packages/proto-plus/testing/constraints-3.9.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/proto-plus/testing/constraints-pypy3.10.txt b/packages/proto-plus/testing/constraints-pypy3.10.txt
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/proto-plus/tests/clam.py b/packages/proto-plus/tests/clam.py
new file mode 100644
index 000000000000..2f9dde13bd73
--- /dev/null
+++ b/packages/proto-plus/tests/clam.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+
+__protobuf__ = proto.module(
+ package="ocean.clam.v1",
+ manifest={
+ "Clam",
+ "Species",
+ "Color",
+ },
+)
+
+
+class Species(proto.Enum):
+ UNKNOWN = 0
+ SQUAMOSA = 1
+ DURASA = 2
+ GIGAS = 3
+
+
+class Color(proto.Enum):
+ COLOR_UNKNOWN = 0
+ BLUE = 1
+ ORANGE = 2
+ GREEN = 3
+
+
+class Clam(proto.Message):
+ species = proto.Field(proto.ENUM, number=1, enum="Species")
+ mass_kg = proto.Field(proto.DOUBLE, number=2)
+ color = proto.Field(proto.ENUM, number=3, enum="Color")
diff --git a/packages/proto-plus/tests/conftest.py b/packages/proto-plus/tests/conftest.py
new file mode 100644
index 000000000000..9f2f5c9540ad
--- /dev/null
+++ b/packages/proto-plus/tests/conftest.py
@@ -0,0 +1,116 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import importlib
+from unittest import mock
+
+from google.protobuf import descriptor_pool
+from google.protobuf import message
+from google.protobuf import reflection
+from google.protobuf import symbol_database
+
+from proto._file_info import _FileInfo
+from proto.marshal import Marshal
+from proto.marshal import rules
+from proto.utils import has_upb
+
+
+def pytest_runtest_setup(item):
+ _FileInfo.registry.clear()
+
+ # Replace the descriptor pool and symbol database to avoid tests
+ # polluting one another.
+ pool = type(descriptor_pool.Default())()
+ sym_db = symbol_database.SymbolDatabase(pool=pool)
+ item._mocks = [
+ mock.patch.object(descriptor_pool, "Default", return_value=pool),
+ mock.patch.object(symbol_database, "Default", return_value=sym_db),
+ ]
+ if descriptor_pool._USE_C_DESCRIPTORS:
+
+ item._mocks.append(
+ mock.patch(
+ (
+ "google._upb._message.default_pool"
+ if has_upb()
+ else "google.protobuf.pyext._message.default_pool"
+ ),
+ pool,
+ )
+ )
+
+ [i.start() for i in item._mocks]
+
+ # Importing a pb2 module registers those messages with the pool.
+ # However, our test harness is subbing out the default pool above,
+ # which means that all the dependencies that messages may depend on
+ # are now absent from the pool.
+ #
+ # Add any pb2 modules that may have been imported by the test's module to
+ # the descriptor pool and symbol database.
+ #
+ # This is exceptionally tricky in the C implementation because there is
+ # no way to add an existing descriptor to a pool; the only acceptable
+ # approach is to add a file descriptor proto, which then creates *new*
+ # descriptors. We therefore do that and then plop the replacement classes
+ # onto the pb2 modules.
+ reloaded = set()
+ for name in dir(item.module):
+ if name.endswith("_pb2") and not name.startswith("test_"):
+ module = getattr(item.module, name)
+
+ # Exclude `google.protobuf.descriptor_pb2` which causes error
+ # `RecursionError: maximum recursion depth exceeded while calling a Python object`
+ # when running the test suite and is not required for tests.
+ # See https://github.com/googleapis/proto-plus-python/issues/425
+ if module.__package__ == "google.protobuf" and name == "descriptor_pb2":
+ continue
+
+ pool.AddSerializedFile(module.DESCRIPTOR.serialized_pb)
+ fd = pool.FindFileByName(module.DESCRIPTOR.name)
+
+ # Register all the messages to the symbol database and the
+ # module. Do this recursively if there are nested messages.
+ _register_messages(module, fd.message_types_by_name, sym_db)
+
+ # Track which modules had new message classes loaded.
+ # This is used below to wire the new classes into the marshal.
+ reloaded.add(name)
+
+ # If the marshal had previously registered the old message classes,
+ # then reload the appropriate modules so the marshal is using the new ones.
+ if "wrappers_pb2" in reloaded:
+ importlib.reload(rules.wrappers)
+ if "struct_pb2" in reloaded:
+ importlib.reload(rules.struct)
+ if reloaded.intersection({"timestamp_pb2", "duration_pb2"}):
+ importlib.reload(rules.dates)
+
+
+def pytest_runtest_teardown(item):
+ Marshal._instances.clear()
+ [i.stop() for i in item._mocks]
+
+
+def _register_messages(scope, iterable, sym_db):
+ """Create and register messages from the file descriptor."""
+ for name, descriptor in iterable.items():
+ new_msg = reflection.GeneratedProtocolMessageType(
+ name,
+ (message.Message,),
+ {"DESCRIPTOR": descriptor, "__module__": None},
+ )
+ sym_db.RegisterMessage(new_msg)
+ setattr(scope, name, new_msg)
+ _register_messages(new_msg, descriptor.nested_types_by_name, sym_db)
diff --git a/packages/proto-plus/tests/enums_test.py b/packages/proto-plus/tests/enums_test.py
new file mode 100644
index 000000000000..59c5e67116b0
--- /dev/null
+++ b/packages/proto-plus/tests/enums_test.py
@@ -0,0 +1,33 @@
+# Copyright (C) 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+
+__protobuf__ = proto.module(
+ package="test.proto",
+ manifest={
+ "Enums",
+ },
+)
+
+
+class OneEnum(proto.Enum):
+ UNSPECIFIED = 0
+ SOME_VALUE = 1
+
+
+class OtherEnum(proto.Enum):
+ UNSPECIFIED = 0
+ APPLE = 1
+ BANANA = 2
diff --git a/packages/proto-plus/tests/mollusc.py b/packages/proto-plus/tests/mollusc.py
new file mode 100644
index 000000000000..c92f161a406d
--- /dev/null
+++ b/packages/proto-plus/tests/mollusc.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+import zone
+
+__protobuf__ = proto.module(
+ package="ocean.mollusc.v1",
+ manifest={
+ "Mollusc",
+ },
+)
+
+
+class Mollusc(proto.Message):
+ zone = proto.Field(zone.Zone, number=1)
diff --git a/packages/proto-plus/tests/test_datetime_helpers.py b/packages/proto-plus/tests/test_datetime_helpers.py
new file mode 100644
index 000000000000..264b5296980a
--- /dev/null
+++ b/packages/proto-plus/tests/test_datetime_helpers.py
@@ -0,0 +1,289 @@
+# Copyright 2017, Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import calendar
+import datetime
+
+import pytest
+import pytz
+
+from proto import datetime_helpers
+from google.protobuf import timestamp_pb2
+
+
+ONE_MINUTE_IN_MICROSECONDS = 60 * 1e6
+
+
+def test_from_microseconds():
+ five_mins_from_epoch_in_microseconds = 5 * ONE_MINUTE_IN_MICROSECONDS
+ five_mins_from_epoch_datetime = datetime.datetime(
+ 1970, 1, 1, 0, 5, 0, tzinfo=datetime.timezone.utc
+ )
+
+ result = datetime_helpers._from_microseconds(five_mins_from_epoch_in_microseconds)
+
+ assert result == five_mins_from_epoch_datetime
+
+
+def test_to_rfc3339():
+ value = datetime.datetime(2016, 4, 5, 13, 30, 0)
+ expected = "2016-04-05T13:30:00.000000Z"
+ assert datetime_helpers._to_rfc3339(value) == expected
+
+
+def test_to_rfc3339_with_utc():
+ value = datetime.datetime(2016, 4, 5, 13, 30, 0, tzinfo=datetime.timezone.utc)
+ expected = "2016-04-05T13:30:00.000000Z"
+ assert datetime_helpers._to_rfc3339(value, ignore_zone=False) == expected
+
+
+def test_to_rfc3339_with_non_utc():
+ zone = pytz.FixedOffset(-60)
+ value = datetime.datetime(2016, 4, 5, 13, 30, 0, tzinfo=zone)
+ expected = "2016-04-05T14:30:00.000000Z"
+ assert datetime_helpers._to_rfc3339(value, ignore_zone=False) == expected
+
+
+def test_to_rfc3339_with_non_utc_ignore_zone():
+ zone = pytz.FixedOffset(-60)
+ value = datetime.datetime(2016, 4, 5, 13, 30, 0, tzinfo=zone)
+ expected = "2016-04-05T13:30:00.000000Z"
+ assert datetime_helpers._to_rfc3339(value, ignore_zone=True) == expected
+
+
+def test_ctor_wo_nanos():
+ stamp = datetime_helpers.DatetimeWithNanoseconds(2016, 12, 20, 21, 13, 47, 123456)
+ assert stamp.year == 2016
+ assert stamp.month == 12
+ assert stamp.day == 20
+ assert stamp.hour == 21
+ assert stamp.minute == 13
+ assert stamp.second == 47
+ assert stamp.microsecond == 123456
+ assert stamp.nanosecond == 123456000
+
+
+def test_ctor_w_nanos():
+ stamp = datetime_helpers.DatetimeWithNanoseconds(
+ 2016, 12, 20, 21, 13, 47, nanosecond=123456789
+ )
+ assert stamp.year == 2016
+ assert stamp.month == 12
+ assert stamp.day == 20
+ assert stamp.hour == 21
+ assert stamp.minute == 13
+ assert stamp.second == 47
+ assert stamp.microsecond == 123456
+ assert stamp.nanosecond == 123456789
+
+
+def test_ctor_w_micros_positional_and_nanos():
+ with pytest.raises(TypeError):
+ datetime_helpers.DatetimeWithNanoseconds(
+ 2016, 12, 20, 21, 13, 47, 123456, nanosecond=123456789
+ )
+
+
+def test_ctor_w_micros_keyword_and_nanos():
+ with pytest.raises(TypeError):
+ datetime_helpers.DatetimeWithNanoseconds(
+ 2016, 12, 20, 21, 13, 47, microsecond=123456, nanosecond=123456789
+ )
+
+
+def test_rfc3339_wo_nanos():
+ stamp = datetime_helpers.DatetimeWithNanoseconds(2016, 12, 20, 21, 13, 47, 123456)
+ assert stamp.rfc3339() == "2016-12-20T21:13:47.123456Z"
+
+
+def test_rfc3339_wo_nanos_w_leading_zero():
+ stamp = datetime_helpers.DatetimeWithNanoseconds(2016, 12, 20, 21, 13, 47, 1234)
+ assert stamp.rfc3339() == "2016-12-20T21:13:47.001234Z"
+
+
+def test_rfc3339_w_nanos():
+ stamp = datetime_helpers.DatetimeWithNanoseconds(
+ 2016, 12, 20, 21, 13, 47, nanosecond=123456789
+ )
+ assert stamp.rfc3339() == "2016-12-20T21:13:47.123456789Z"
+
+
+def test_rfc3339_w_nanos_w_leading_zero():
+ stamp = datetime_helpers.DatetimeWithNanoseconds(
+ 2016, 12, 20, 21, 13, 47, nanosecond=1234567
+ )
+ assert stamp.rfc3339() == "2016-12-20T21:13:47.001234567Z"
+
+
+def test_rfc3339_w_nanos_no_trailing_zeroes():
+ stamp = datetime_helpers.DatetimeWithNanoseconds(
+ 2016, 12, 20, 21, 13, 47, nanosecond=100000000
+ )
+ assert stamp.rfc3339() == "2016-12-20T21:13:47.1Z"
+
+
+def test_rfc3339_w_nanos_w_leading_zero_and_no_trailing_zeros():
+ stamp = datetime_helpers.DatetimeWithNanoseconds(
+ 2016, 12, 20, 21, 13, 47, nanosecond=1234500
+ )
+ assert stamp.rfc3339() == "2016-12-20T21:13:47.0012345Z"
+
+
+def test_from_rfc3339_w_invalid():
+ stamp = "2016-12-20T21:13:47"
+ with pytest.raises(ValueError):
+ datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(stamp)
+
+
+def test_from_rfc3339_wo_fraction():
+ timestamp = "2016-12-20T21:13:47Z"
+ expected = datetime_helpers.DatetimeWithNanoseconds(
+ 2016, 12, 20, 21, 13, 47, tzinfo=datetime.timezone.utc
+ )
+ stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
+ assert stamp == expected
+
+
+def test_from_rfc3339_w_partial_precision():
+ timestamp = "2016-12-20T21:13:47.1Z"
+ expected = datetime_helpers.DatetimeWithNanoseconds(
+ 2016, 12, 20, 21, 13, 47, microsecond=100000, tzinfo=datetime.timezone.utc
+ )
+ stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
+ assert stamp == expected
+
+
+def test_from_rfc3339_w_full_precision():
+ timestamp = "2016-12-20T21:13:47.123456789Z"
+ expected = datetime_helpers.DatetimeWithNanoseconds(
+ 2016, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=datetime.timezone.utc
+ )
+ stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
+ assert stamp == expected
+
+
+@pytest.mark.parametrize(
+ "fractional, nanos",
+ [
+ ("12345678", 123456780),
+ ("1234567", 123456700),
+ ("123456", 123456000),
+ ("12345", 123450000),
+ ("1234", 123400000),
+ ("123", 123000000),
+ ("12", 120000000),
+ ("1", 100000000),
+ ],
+)
+def test_from_rfc3339_test_nanoseconds(fractional, nanos):
+ value = "2009-12-17T12:44:32.{}Z".format(fractional)
+ assert (
+ datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(value).nanosecond == nanos
+ )
+
+
+def test_timestamp_pb_wo_nanos_naive():
+ stamp = datetime_helpers.DatetimeWithNanoseconds(2016, 12, 20, 21, 13, 47, 123456)
+ delta = stamp.replace(tzinfo=datetime.timezone.utc) - datetime_helpers._UTC_EPOCH
+ seconds = int(delta.total_seconds())
+ nanos = 123456000
+ timestamp = timestamp_pb2.Timestamp(seconds=seconds, nanos=nanos)
+ assert stamp.timestamp_pb() == timestamp
+
+
+def test_timestamp_pb_w_nanos():
+ stamp = datetime_helpers.DatetimeWithNanoseconds(
+ 2016, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=datetime.timezone.utc
+ )
+ delta = stamp - datetime_helpers._UTC_EPOCH
+ timestamp = timestamp_pb2.Timestamp(
+ seconds=int(delta.total_seconds()), nanos=123456789
+ )
+ assert stamp.timestamp_pb() == timestamp
+
+
+def test_from_timestamp_pb_wo_nanos():
+ when = datetime.datetime(
+ 2016, 12, 20, 21, 13, 47, 123456, tzinfo=datetime.timezone.utc
+ )
+ delta = when - datetime_helpers._UTC_EPOCH
+ seconds = int(delta.total_seconds())
+ timestamp = timestamp_pb2.Timestamp(seconds=seconds)
+
+ stamp = datetime_helpers.DatetimeWithNanoseconds.from_timestamp_pb(timestamp)
+
+ assert _to_seconds(when) == _to_seconds(stamp)
+ assert stamp.microsecond == 0
+ assert stamp.nanosecond == 0
+ assert stamp.tzinfo == datetime.timezone.utc
+
+
+def test_replace():
+ stamp = datetime_helpers.DatetimeWithNanoseconds(
+ 2016, 12, 20, 21, 13, 47, 123456, tzinfo=datetime.timezone.utc
+ )
+
+ # ns and ms provided raises
+ with pytest.raises(TypeError):
+ stamp.replace(microsecond=1, nanosecond=0)
+
+ # No Nanoseconds or Microseconds
+ new_stamp = stamp.replace(year=2015)
+ assert new_stamp.year == 2015
+ assert new_stamp.microsecond == 123456
+ assert new_stamp.nanosecond == 123456000
+
+ # Nanos
+ new_stamp = stamp.replace(nanosecond=789123)
+ assert new_stamp.microsecond == 789
+ assert new_stamp.nanosecond == 789123
+
+ # Micros
+ new_stamp = stamp.replace(microsecond=456)
+ assert new_stamp.microsecond == 456
+ assert new_stamp.nanosecond == 456000
+
+ # assert _to_seconds(when) == _to_seconds(stamp)
+ # assert stamp.microsecond == 0
+ # assert stamp.nanosecond == 0
+ # assert stamp.tzinfo == datetime.timezone.utc
+
+
+def test_from_timestamp_pb_w_nanos():
+ when = datetime.datetime(
+ 2016, 12, 20, 21, 13, 47, 123456, tzinfo=datetime.timezone.utc
+ )
+ delta = when - datetime_helpers._UTC_EPOCH
+ seconds = int(delta.total_seconds())
+ timestamp = timestamp_pb2.Timestamp(seconds=seconds, nanos=123456789)
+
+ stamp = datetime_helpers.DatetimeWithNanoseconds.from_timestamp_pb(timestamp)
+
+ assert _to_seconds(when) == _to_seconds(stamp)
+ assert stamp.microsecond == 123456
+ assert stamp.nanosecond == 123456789
+ assert stamp.tzinfo == datetime.timezone.utc
+
+
+def _to_seconds(value):
+ """Convert a datetime to seconds since the unix epoch.
+
+ Args:
+ value (datetime.datetime): The datetime to convert.
+
+ Returns:
+ int: Microseconds since the unix epoch.
+ """
+ assert value.tzinfo is datetime.timezone.utc
+ return calendar.timegm(value.timetuple())
diff --git a/packages/proto-plus/tests/test_enum_total_ordering.py b/packages/proto-plus/tests/test_enum_total_ordering.py
new file mode 100644
index 000000000000..584a1831c58a
--- /dev/null
+++ b/packages/proto-plus/tests/test_enum_total_ordering.py
@@ -0,0 +1,97 @@
+# Copyright 2021, Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+import enums_test
+
+
+def test_total_ordering_w_same_enum_type():
+ to_compare = enums_test.OneEnum.SOME_VALUE
+
+ for item in enums_test.OneEnum:
+ if item.value < to_compare.value:
+ assert not to_compare == item
+ assert to_compare != item
+ assert not to_compare < item
+ assert not to_compare <= item
+ assert to_compare > item
+ assert to_compare >= item
+ elif item.value > to_compare.value:
+ assert not to_compare == item
+ assert to_compare != item
+ assert to_compare < item
+ assert to_compare <= item
+ assert not to_compare > item
+ assert not to_compare >= item
+ else: # item.value == to_compare.value:
+ assert to_compare == item
+ assert not to_compare != item
+ assert not to_compare < item
+ assert to_compare <= item
+ assert not to_compare > item
+ assert to_compare >= item
+
+
+def test_total_ordering_w_other_enum_type():
+ to_compare = enums_test.OneEnum.SOME_VALUE
+
+ for item in enums_test.OtherEnum:
+ assert not to_compare == item
+ assert type(to_compare).SOME_VALUE != item
+ try:
+ assert to_compare.SOME_VALUE != item
+ except AttributeError: # Python 3.11.0b3
+ pass
+ with pytest.raises(TypeError):
+ assert not to_compare < item
+ with pytest.raises(TypeError):
+ assert not to_compare <= item
+ with pytest.raises(TypeError):
+ assert not to_compare > item
+ with pytest.raises(TypeError):
+ assert not to_compare >= item
+
+
+@pytest.mark.parametrize("int_val", range(-1, 3))
+def test_total_ordering_w_int(int_val):
+ to_compare = enums_test.OneEnum.SOME_VALUE
+
+ if int_val < to_compare.value:
+ assert not to_compare == int_val
+ assert to_compare != int_val
+ assert not to_compare < int_val
+ assert not to_compare <= int_val
+ assert to_compare > int_val
+ assert to_compare >= int_val
+ elif int_val > to_compare.value:
+ assert not to_compare == int_val
+ assert to_compare != int_val
+ assert to_compare < int_val
+ assert to_compare <= int_val
+ assert not to_compare > int_val
+ assert not to_compare >= int_val
+ else: # int_val == to_compare.value:
+ assert to_compare == int_val
+ assert not to_compare != int_val
+ assert not to_compare < int_val
+ assert to_compare <= int_val
+ assert not to_compare > int_val
+ assert to_compare >= int_val
+
+
+def test_hashing():
+ to_hash = enums_test.OneEnum.SOME_VALUE
+
+ {to_hash: "testing"} # no raise
diff --git a/packages/proto-plus/tests/test_fields_bytes.py b/packages/proto-plus/tests/test_fields_bytes.py
new file mode 100644
index 000000000000..625dcd91c404
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_bytes.py
@@ -0,0 +1,96 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import pytest
+
+import proto
+
+
+def test_bytes_init():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.BYTES, number=1)
+ baz = proto.Field(proto.BYTES, number=2)
+
+ foo = Foo(bar=b"spam")
+ assert foo.bar == b"spam"
+ assert foo.baz == b""
+ assert not foo.baz
+ assert Foo.pb(foo).bar == b"spam"
+ assert Foo.pb(foo).baz == b""
+
+
+def test_bytes_rmw():
+ class Foo(proto.Message):
+ spam = proto.Field(proto.BYTES, number=1)
+ eggs = proto.Field(proto.BYTES, number=2)
+
+ foo = Foo(spam=b"bar")
+ foo.eggs = b"baz"
+ assert foo.spam == b"bar"
+ assert foo.eggs == b"baz"
+ assert Foo.pb(foo).spam == b"bar"
+ assert Foo.pb(foo).eggs == b"baz"
+ foo.spam = b"bacon"
+ assert foo.spam == b"bacon"
+ assert foo.eggs == b"baz"
+ assert Foo.pb(foo).spam == b"bacon"
+ assert Foo.pb(foo).eggs == b"baz"
+
+
+def test_bytes_del():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.BYTES, number=1)
+
+ foo = Foo(bar=b"spam")
+ assert foo.bar == b"spam"
+ del foo.bar
+ assert foo.bar == b""
+ assert not foo.bar
+
+
+def test_bytes_string_distinct():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.STRING, number=1)
+ baz = proto.Field(proto.BYTES, number=2)
+
+ foo = Foo()
+ assert foo.bar != foo.baz
+
+ # Since protobuf was written against Python 2, it accepts bytes objects
+ # for strings (but not vice versa).
+ foo.bar = b"anything"
+ assert foo.bar == "anything"
+
+ # We need to permit setting bytes fields from strings,
+ # but the marshalling needs to base64 decode the result.
+ # This is a requirement for interop with the vanilla protobuf runtime:
+ # converting a proto message to a dict base64 encodes the bytes
+ # because it may be sent over the network via a protocol like HTTP.
+ encoded_swallow: str = base64.urlsafe_b64encode(b"unladen swallow").decode("utf-8")
+ assert type(encoded_swallow) == str
+ foo.baz = encoded_swallow
+ assert foo.baz == b"unladen swallow"
+
+
+def test_bytes_to_dict_bidi():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.BYTES, number=1)
+
+ foo = Foo(bar=b"spam")
+
+ foo_dict = Foo.to_dict(foo)
+ foo_two = Foo(foo_dict)
+
+ assert foo == foo_two
diff --git a/packages/proto-plus/tests/test_fields_composite.py b/packages/proto-plus/tests/test_fields_composite.py
new file mode 100644
index 000000000000..60d29e122b12
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_composite.py
@@ -0,0 +1,93 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+
+
+def test_composite_init():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.STRING, number=1)
+ baz = proto.Field(proto.INT64, number=2)
+
+ class Spam(proto.Message):
+ foo = proto.Field(proto.MESSAGE, number=1, message=Foo)
+ eggs = proto.Field(proto.BOOL, number=2)
+
+ spam = Spam(foo=Foo(bar="str", baz=42))
+ assert spam.foo.bar == "str"
+ assert spam.foo.baz == 42
+ assert spam.eggs is False
+
+
+def test_composite_inner_rmw():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.STRING, number=1)
+
+ class Spam(proto.Message):
+ foo = proto.Field(proto.MESSAGE, number=1, message=Foo)
+
+ spam = Spam(foo=Foo(bar="str"))
+ spam.foo.bar = "other str"
+ assert spam.foo.bar == "other str"
+ assert Spam.pb(spam).foo.bar == "other str"
+
+
+def test_composite_empty_inner_rmw():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Spam(proto.Message):
+ foo = proto.Field(proto.MESSAGE, number=1, message=Foo)
+
+ spam = Spam()
+ spam.foo.bar = 42
+ assert spam.foo.bar == 42
+
+
+def test_composite_outer_rmw():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.FLOAT, number=1)
+
+ class Spam(proto.Message):
+ foo = proto.Field(proto.MESSAGE, number=1, message=Foo)
+
+ spam = Spam(foo=Foo(bar=3.14159))
+ spam.foo = Foo(bar=2.71828)
+ assert abs(spam.foo.bar - 2.71828) < 1e-7
+
+
+def test_composite_dict_write():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.FLOAT, number=1)
+
+ class Spam(proto.Message):
+ foo = proto.Field(proto.MESSAGE, number=1, message=Foo)
+
+ spam = Spam()
+ spam.foo = {"bar": 2.71828}
+ assert abs(spam.foo.bar - 2.71828) < 1e-7
+
+
+def test_composite_del():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.STRING, number=1)
+
+ class Spam(proto.Message):
+ foo = proto.Field(proto.MESSAGE, number=1, message=Foo)
+
+ spam = Spam(foo=Foo(bar="str"))
+ del spam.foo
+ assert not spam.foo
+ assert isinstance(spam.foo, Foo)
+ assert spam.foo.bar == ""
diff --git a/packages/proto-plus/tests/test_fields_composite_string_ref.py b/packages/proto-plus/tests/test_fields_composite_string_ref.py
new file mode 100644
index 000000000000..b6c6faaa0a47
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_composite_string_ref.py
@@ -0,0 +1,106 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+
+import proto
+
+
+def test_composite_forward_ref():
+ class Spam(proto.Message):
+ foo = proto.Field(proto.MESSAGE, number=1, message="Foo")
+ eggs = proto.Field(proto.BOOL, number=2)
+
+ class Foo(proto.Message):
+ bar = proto.Field(proto.STRING, number=1)
+ baz = proto.Field(proto.INT64, number=2)
+
+ spam = Spam(foo=Foo(bar="str", baz=42))
+ assert spam.foo.bar == "str"
+ assert spam.foo.baz == 42
+ assert spam.eggs is False
+
+
+def test_composite_forward_ref_with_package():
+ sys.modules[__name__].__protobuf__ = proto.module(package="abc.def")
+ try:
+
+ class Spam(proto.Message):
+ foo = proto.Field("Foo", number=1)
+
+ class Eggs(proto.Message):
+ foo = proto.Field("abc.def.Foo", number=1)
+
+ class Foo(proto.Message):
+ bar = proto.Field(proto.STRING, number=1)
+ baz = proto.Field(proto.INT64, number=2)
+
+ finally:
+ del sys.modules[__name__].__protobuf__
+
+ spam = Spam(foo=Foo(bar="str", baz=42))
+ eggs = Eggs(foo=Foo(bar="rts", baz=24))
+ assert spam.foo.bar == "str"
+ assert spam.foo.baz == 42
+ assert eggs.foo.bar == "rts"
+ assert eggs.foo.baz == 24
+
+
+def test_composite_backward_ref():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.STRING, number=1)
+ baz = proto.Field(proto.INT64, number=2)
+
+ class Spam(proto.Message):
+ foo = proto.Field(proto.MESSAGE, number=1, message=Foo)
+ eggs = proto.Field(proto.BOOL, number=2)
+
+ spam = Spam(foo=Foo(bar="str", baz=42))
+ assert spam.foo.bar == "str"
+ assert spam.foo.baz == 42
+ assert spam.eggs is False
+
+
+def test_composite_multi_ref():
+ class Spam(proto.Message):
+ foo = proto.Field(proto.MESSAGE, number=1, message="Foo")
+ eggs = proto.Field(proto.BOOL, number=2)
+
+ class Foo(proto.Message):
+ bar = proto.Field(proto.STRING, number=1)
+ baz = proto.Field(proto.INT64, number=2)
+
+ class Bacon(proto.Message):
+ foo = proto.Field(proto.MESSAGE, number=1, message=Foo)
+
+ spam = Spam(foo=Foo(bar="str", baz=42))
+ bacon = Bacon(foo=spam.foo)
+ assert spam.foo.bar == "str"
+ assert spam.foo.baz == 42
+ assert spam.eggs is False
+ assert bacon.foo == spam.foo
+
+
+def test_composite_self_ref():
+ class Spam(proto.Message):
+ spam = proto.Field(proto.MESSAGE, number=1, message="Spam")
+ eggs = proto.Field(proto.BOOL, number=2)
+
+ spam = Spam(spam=Spam(eggs=True))
+ assert spam.eggs is False
+ assert spam.spam.eggs is True
+ assert type(spam) is type(spam.spam) # noqa: E0721
+ assert not spam.spam.spam
+ assert spam.spam.spam.eggs is False
+ assert not spam.spam.spam.spam.spam.spam.spam.spam
diff --git a/packages/proto-plus/tests/test_fields_enum.py b/packages/proto-plus/tests/test_fields_enum.py
new file mode 100644
index 000000000000..e5d5b324a285
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_enum.py
@@ -0,0 +1,394 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import proto
+import pytest
+import sys
+
+
+def test_outer_enum_init():
+ class Foo(proto.Message):
+ color = proto.Field(proto.ENUM, number=1, enum="Color")
+
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ foo = Foo(color=Color.RED)
+ assert foo.color == Color.RED
+ assert foo.color == 1
+ assert foo.color
+ assert isinstance(foo.color, Color)
+ assert Foo.pb(foo).color == 1
+
+
+def test_outer_enum_init_int():
+ class Foo(proto.Message):
+ color = proto.Field(proto.ENUM, number=1, enum="Color")
+
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ foo = Foo(color=1)
+ assert foo.color == Color.RED
+ assert foo.color == 1
+ assert foo.color
+ assert isinstance(foo.color, Color)
+ assert Foo.pb(foo).color == 1
+
+
+def test_outer_enum_init_str():
+ class Foo(proto.Message):
+ color = proto.Field(proto.ENUM, number=1, enum="Color")
+
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ foo = Foo(color="RED")
+ assert foo.color == Color.RED
+ assert foo.color == 1
+ assert foo.color
+ assert isinstance(foo.color, Color)
+ assert Foo.pb(foo).color == 1
+
+
+def test_outer_enum_init_dict():
+ class Foo(proto.Message):
+ color = proto.Field(proto.ENUM, number=1, enum="Color")
+
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ foo = Foo({"color": 1})
+ assert foo.color == Color.RED
+ assert foo.color == 1
+ assert foo.color
+ assert isinstance(foo.color, Color)
+ assert Foo.pb(foo).color == 1
+
+
+def test_outer_enum_init_dict_str():
+ class Foo(proto.Message):
+ color = proto.Field(proto.ENUM, number=1, enum="Color")
+
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ foo = Foo({"color": "BLUE"})
+ assert foo.color == Color.BLUE
+ assert foo.color == 3
+ assert foo.color
+ assert isinstance(foo.color, Color)
+ assert Foo.pb(foo).color == 3
+
+
+def test_outer_enum_init_pb2():
+ class Foo(proto.Message):
+ color = proto.Field(proto.ENUM, number=1, enum="Color")
+
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ foo = Foo(Foo.pb()(color=Color.RED))
+ assert foo.color == Color.RED
+ assert foo.color == 1
+ assert foo.color
+ assert isinstance(foo.color, Color)
+ assert Foo.pb(foo).color == 1
+
+
+def test_outer_enum_unset():
+ class Foo(proto.Message):
+ color = proto.Field(proto.ENUM, number=1, enum="Color")
+
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ foo = Foo()
+ assert foo.color == Color.COLOR_UNSPECIFIED
+ assert foo.color == 0
+ assert "color" not in foo
+ assert not foo.color
+ assert Foo.pb(foo).color == 0
+ assert Foo.serialize(foo) == b""
+
+
+def test_outer_enum_write():
+ class Foo(proto.Message):
+ color = proto.Field(proto.ENUM, number=1, enum="Color")
+
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ foo = Foo()
+ foo.color = Color.GREEN
+ assert foo.color == Color.GREEN
+ assert foo.color == 2
+ assert Foo.pb(foo).color == 2
+ assert foo.color
+
+
+def test_outer_enum_write_int():
+ class Foo(proto.Message):
+ color = proto.Field(proto.ENUM, number=1, enum="Color")
+
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ foo = Foo()
+ foo.color = 3
+ assert foo.color == Color.BLUE
+ assert foo.color == 3
+ assert isinstance(foo.color, Color)
+ assert Foo.pb(foo).color == 3
+ assert foo.color
+
+
+def test_outer_enum_write_str():
+ class Foo(proto.Message):
+ color = proto.Field(proto.ENUM, number=1, enum="Color")
+
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ foo = Foo()
+ foo.color = "BLUE"
+ assert foo.color == Color.BLUE
+ assert foo.color == 3
+ assert isinstance(foo.color, Color)
+ assert Foo.pb(foo).color == 3
+ assert foo.color
+
+
+def test_inner_enum_init():
+ class Foo(proto.Message):
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ color = proto.Field(Color, number=1)
+
+ foo = Foo(color=Foo.Color.RED)
+ assert foo.color == Foo.Color.RED
+ assert foo.color == 1
+ assert foo.color
+ assert Foo.pb(foo).color == 1
+
+
+def test_inner_enum_write():
+ class Foo(proto.Message):
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ color = proto.Field(Color, number=1)
+
+ foo = Foo()
+ foo.color = Foo.Color.GREEN
+ assert foo.color == Foo.Color.GREEN
+ assert foo.color == 2
+ assert isinstance(foo.color, Foo.Color)
+ assert Foo.pb(foo).color == 2
+ assert foo.color
+
+
+def test_enum_del():
+ class Foo(proto.Message):
+ color = proto.Field(proto.ENUM, number=1, enum="Color")
+
+ class Color(proto.Enum):
+ COLOR_UNSPECIFIED = 0
+ RED = 1
+ GREEN = 2
+ BLUE = 3
+
+ foo = Foo(color=Color.BLUE)
+ del foo.color
+ assert foo.color == Color.COLOR_UNSPECIFIED
+ assert foo.color == 0
+ assert isinstance(foo.color, Color)
+ assert "color" not in foo
+ assert not foo.color
+ assert Foo.pb(foo).color == 0
+
+
+def test_nested_enum_from_string():
+ class Trawl(proto.Message):
+ # Note: this indirection with the nested field
+ # is necessary to trigger the exception for testing.
+ # Setting the field in an existing message accepts strings AND
+ # checks for valid variants.
+ # Similarly, constructing a message directly with a top level
+ # enum field kwarg passed as a string is also handled correctly, i.e.
+ # s = Squid(zone="ABYSSOPELAGIC")
+ # does NOT raise an exception.
+ squids = proto.RepeatedField("Squid", number=1)
+
+ class Squid(proto.Message):
+ zone = proto.Field(proto.ENUM, number=1, enum="Zone")
+
+ class Zone(proto.Enum):
+ EPIPELAGIC = 0
+ MESOPELAGIC = 1
+ BATHYPELAGIC = 2
+ ABYSSOPELAGIC = 3
+
+ t = Trawl(squids=[{"zone": "MESOPELAGIC"}])
+ assert t.squids[0] == Squid(zone=Zone.MESOPELAGIC)
+
+
+def test_enum_field_by_string():
+ class Squid(proto.Message):
+ zone = proto.Field(proto.ENUM, number=1, enum="Zone")
+
+ class Zone(proto.Enum):
+ EPIPELAGIC = 0
+ MESOPELAGIC = 1
+ BATHYPELAGIC = 2
+ ABYSSOPELAGIC = 3
+
+ s = Squid(zone=Zone.BATHYPELAGIC)
+ assert s.zone == Zone.BATHYPELAGIC
+
+
+def test_enum_field_by_string_with_package():
+ sys.modules[__name__].__protobuf__ = proto.module(package="mollusca.cephalopoda")
+ try:
+
+ class Octopus(proto.Message):
+ zone = proto.Field(proto.ENUM, number=1, enum="mollusca.cephalopoda.Zone")
+
+ class Zone(proto.Enum):
+ EPIPELAGIC = 0
+ MESOPELAGIC = 1
+ BATHYPELAGIC = 2
+ ABYSSOPELAGIC = 3
+
+ finally:
+ del sys.modules[__name__].__protobuf__
+
+ o = Octopus(zone="MESOPELAGIC")
+ assert o.zone == Zone.MESOPELAGIC
+
+
+def test_enums_in_different_files():
+ import mollusc
+ import zone
+
+ m = mollusc.Mollusc(zone="BATHYPELAGIC")
+
+ assert m.zone == zone.Zone.BATHYPELAGIC
+
+
+def test_enums_in_one_file():
+ import clam
+
+ c = clam.Clam(species=clam.Species.DURASA)
+ assert c.species == clam.Species.DURASA
+
+
+def test_unwrapped_enum_fields():
+ # The dayofweek_pb2 module apparently does some things that are deprecated
+ # in the protobuf API.
+ # There's nothing we can do about that, so ignore it.
+ import warnings
+
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
+
+ from google.type import dayofweek_pb2 as dayofweek
+
+ class Event(proto.Message):
+ weekday = proto.Field(proto.ENUM, number=1, enum=dayofweek.DayOfWeek)
+
+ e = Event(weekday="WEDNESDAY")
+ e2 = Event.deserialize(Event.serialize(e))
+ assert e == e2
+
+ class Task(proto.Message):
+ weekday = proto.Field(dayofweek.DayOfWeek, number=1)
+
+ t = Task(weekday="TUESDAY")
+ t2 = Task.deserialize(Task.serialize(t))
+ assert t == t2
+
+
+if os.environ.get("PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION", "python") == "cpp":
+ # This test only works, and is only relevant, with the cpp runtime.
+ # Python just doesn't give a care and lets it work anyway.
+ def test_enum_alias_bad():
+ # Certain enums may shadow the different enum monikers with the same value.
+ # This is generally discouraged, and protobuf will object by default,
+ # but will explicitly allow this behavior if the enum is defined with
+ # the `allow_alias` option set.
+ with pytest.raises(TypeError):
+
+ # The wrapper message is a hack to avoid manifest wrangling to
+ # define the enum.
+ class BadMessage(proto.Message):
+ class BadEnum(proto.Enum):
+ UNKNOWN = 0
+ DEFAULT = 0
+
+ bad_dup_enum = proto.Field(proto.ENUM, number=1, enum=BadEnum)
+
+
+def test_enum_alias_good():
+ # Have to split good and bad enum alias into two tests so that the generated
+ # file descriptor is properly created.
+ # For the python runtime, aliases are allowed by default, but we want to
+ # make sure that the options don't cause problems.
+ # For the cpp runtime, we need to verify that we can in fact define aliases.
+ class GoodMessage(proto.Message):
+ class GoodEnum(proto.Enum):
+ _pb_options = {"allow_alias": True}
+ UNKNOWN = 0
+ DEFAULT = 0
+
+ good_dup_enum = proto.Field(proto.ENUM, number=1, enum=GoodEnum)
+
+ assert GoodMessage.GoodEnum.UNKNOWN == GoodMessage.GoodEnum.DEFAULT == 0
diff --git a/packages/proto-plus/tests/test_fields_int.py b/packages/proto-plus/tests/test_fields_int.py
new file mode 100644
index 000000000000..81e15e4e73ca
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_int.py
@@ -0,0 +1,138 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+import proto
+
+
+def test_int_init():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+ baz = proto.Field(proto.INT32, number=2)
+
+ foo = Foo(bar=42)
+ assert foo.bar == 42
+ assert foo.baz == 0
+ assert not foo.baz
+ assert Foo.pb(foo).bar == 42
+ assert Foo.pb(foo).baz == 0
+
+
+def test_int_rmw():
+ class Foo(proto.Message):
+ spam = proto.Field(proto.INT32, number=1)
+ eggs = proto.Field(proto.INT32, number=2)
+
+ foo = Foo(spam=42)
+ foo.eggs = 76 # trombones led the big parade...
+ assert foo.spam == 42
+ assert foo.eggs == 76
+ assert Foo.pb(foo).spam == 42
+ assert Foo.pb(foo).eggs == 76
+ foo.spam = 144
+ assert foo.spam == 144
+ assert foo.eggs == 76
+ assert Foo.pb(foo).spam == 144
+ assert Foo.pb(foo).eggs == 76
+
+
+def test_int_del():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ foo = Foo(bar=42)
+ assert foo.bar == 42
+ del foo.bar
+ assert foo.bar == 0
+ assert not foo.bar
+
+
+def test_int_size():
+ class Foo(proto.Message):
+ small = proto.Field(proto.INT32, number=1)
+ big = proto.Field(proto.INT64, number=2)
+
+ foo = Foo()
+ foo.big = 2**40
+ assert foo.big == 2**40
+ with pytest.raises(ValueError):
+ foo.small = 2**40
+ with pytest.raises(ValueError):
+ Foo(small=2**40)
+
+
+def test_int_unsigned():
+ class Foo(proto.Message):
+ signed = proto.Field(proto.INT32, number=1)
+ unsigned = proto.Field(proto.UINT32, number=2)
+
+ foo = Foo()
+ foo.signed = -10
+ assert foo.signed == -10
+ with pytest.raises(ValueError):
+ foo.unsigned = -10
+ with pytest.raises(ValueError):
+ Foo(unsigned=-10)
+
+
+def test_field_descriptor_idempotent():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ bar_field = Foo.meta.fields["bar"]
+ assert bar_field.descriptor is bar_field.descriptor
+
+
+def test_int64_dict_round_trip():
+ # When converting a message to other types, protobuf turns int64 fields
+ # into decimal coded strings.
+ # This is not a problem for round trip JSON, but it is a problem
+ # when doing a round trip conversion from a message to a dict to a message.
+ # See https://github.com/protocolbuffers/protobuf/issues/2679
+ # and
+ # https://developers.google.com/protocol-buffers/docs/proto3#json
+ # for more details.
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT64, number=1)
+ length_cm = proto.Field(proto.UINT64, number=2)
+ age_s = proto.Field(proto.FIXED64, number=3)
+ depth_m = proto.Field(proto.SFIXED64, number=4)
+ serial_num = proto.Field(proto.SINT64, number=5)
+
+ s = Squid(mass_kg=10, length_cm=20, age_s=30, depth_m=40, serial_num=50)
+
+ s_dict = Squid.to_dict(s)
+
+ s2 = Squid(s_dict)
+
+ assert s == s2
+
+ # Double check that the conversion works with deeply nested messages.
+ class Clam(proto.Message):
+ class Shell(proto.Message):
+ class Pearl(proto.Message):
+ mass_kg = proto.Field(proto.INT64, number=1)
+
+ pearl = proto.Field(Pearl, number=1)
+
+ shell = proto.Field(Shell, number=1)
+
+ c = Clam(shell=Clam.Shell(pearl=Clam.Shell.Pearl(mass_kg=10)))
+
+ c_dict = Clam.to_dict(c)
+
+ c2 = Clam(c_dict)
+
+ assert c == c2
diff --git a/packages/proto-plus/tests/test_fields_map_composite.py b/packages/proto-plus/tests/test_fields_map_composite.py
new file mode 100644
index 000000000000..3b2d8fd9acd6
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_map_composite.py
@@ -0,0 +1,117 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+import proto
+
+
+def test_composite_map():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.MapField(
+ proto.STRING,
+ proto.MESSAGE,
+ number=1,
+ message=Foo,
+ )
+
+ baz = Baz(foos={"i": Foo(bar=42), "j": Foo(bar=24)})
+ assert len(baz.foos) == 2
+ assert baz.foos["i"].bar == 42
+ assert baz.foos["j"].bar == 24
+ assert "k" not in baz.foos
+
+
+def test_composite_map_dict():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.MapField(
+ proto.STRING,
+ proto.MESSAGE,
+ number=1,
+ message=Foo,
+ )
+
+ baz = Baz(foos={"i": {"bar": 42}, "j": {"bar": 24}})
+ assert len(baz.foos) == 2
+ assert baz.foos["i"].bar == 42
+ assert baz.foos["j"].bar == 24
+ assert "k" not in baz.foos
+ with pytest.raises(KeyError):
+ baz.foos["k"]
+
+
+def test_composite_map_set():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.MapField(
+ proto.STRING,
+ proto.MESSAGE,
+ number=1,
+ message=Foo,
+ )
+
+ baz = Baz()
+ baz.foos["i"] = Foo(bar=42)
+ baz.foos["j"] = Foo(bar=24)
+ assert len(baz.foos) == 2
+ assert baz.foos["i"].bar == 42
+ assert baz.foos["j"].bar == 24
+ assert "k" not in baz.foos
+
+
+def test_composite_map_deep_set():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.MapField(
+ proto.STRING,
+ proto.MESSAGE,
+ number=1,
+ message=Foo,
+ )
+
+ baz = Baz()
+ baz.foos["i"] = Foo()
+ baz.foos["i"].bar = 42
+ assert len(baz.foos) == 1
+ assert baz.foos["i"].bar == 42
+
+
+def test_composite_map_del():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.MapField(
+ proto.STRING,
+ proto.MESSAGE,
+ number=1,
+ message=Foo,
+ )
+
+ baz = Baz()
+ baz.foos["i"] = Foo(bar=42)
+ assert len(baz.foos) == 1
+ del baz.foos["i"]
+ assert len(baz.foos) == 0
+ assert "i" not in baz.foos
diff --git a/packages/proto-plus/tests/test_fields_map_scalar.py b/packages/proto-plus/tests/test_fields_map_scalar.py
new file mode 100644
index 000000000000..25ce495629b4
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_map_scalar.py
@@ -0,0 +1,58 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+
+
+def test_basic_map():
+ class Foo(proto.Message):
+ tags = proto.MapField(proto.STRING, proto.STRING, number=1)
+
+ foo = Foo(tags={"a": "foo", "b": "bar"})
+ assert foo.tags["a"] == "foo"
+ assert foo.tags["b"] == "bar"
+ assert "c" not in foo.tags
+
+
+def test_basic_map_with_underscore_field_name():
+ class Foo(proto.Message):
+ tag_labels = proto.MapField(proto.STRING, proto.STRING, number=1)
+
+ foo = Foo(tag_labels={"a": "foo", "b": "bar"})
+ assert foo.tag_labels["a"] == "foo"
+ assert foo.tag_labels["b"] == "bar"
+ assert "c" not in foo.tag_labels
+
+
+def test_basic_map_assignment():
+ class Foo(proto.Message):
+ tags = proto.MapField(proto.STRING, proto.STRING, number=1)
+
+ foo = Foo(tags={"a": "foo"})
+ foo.tags["b"] = "bar"
+ assert len(foo.tags) == 2
+ assert foo.tags["a"] == "foo"
+ assert foo.tags["b"] == "bar"
+ assert "c" not in foo.tags
+
+
+def test_basic_map_deletion():
+ class Foo(proto.Message):
+ tags = proto.MapField(proto.STRING, proto.STRING, number=1)
+
+ foo = Foo(tags={"a": "foo", "b": "bar"})
+ del foo.tags["b"]
+ assert len(foo.tags) == 1
+ assert foo.tags["a"] == "foo"
+ assert "b" not in foo.tags
diff --git a/packages/proto-plus/tests/test_fields_mitigate_collision.py b/packages/proto-plus/tests/test_fields_mitigate_collision.py
new file mode 100644
index 000000000000..117af48ac241
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_mitigate_collision.py
@@ -0,0 +1,81 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+import pytest
+
+# Underscores may be appended to field names
+# that collide with python or proto-plus keywords.
+# In case a key only exists with a `_` suffix, coerce the key
+# to include the `_` suffix. It's not possible to
+# natively define the same field with a trailing underscore in protobuf.
+# See related issue
+# https://github.com/googleapis/python-api-core/issues/227
+def test_fields_mitigate_collision():
+ class TestMessage(proto.Message):
+ spam_ = proto.Field(proto.STRING, number=1)
+ eggs = proto.Field(proto.STRING, number=2)
+
+ class TextStream(proto.Message):
+ text_stream = proto.Field(TestMessage, number=1)
+
+ obj = TestMessage(spam_="has_spam")
+ obj.eggs = "has_eggs"
+ assert obj.spam_ == "has_spam"
+
+ # Test that `spam` is coerced to `spam_`
+ modified_obj = TestMessage({"spam": "has_spam", "eggs": "has_eggs"})
+ assert modified_obj.spam_ == "has_spam"
+
+ # Test get and set
+ modified_obj.spam = "no_spam"
+ assert modified_obj.spam == "no_spam"
+
+ modified_obj.spam_ = "yes_spam"
+ assert modified_obj.spam_ == "yes_spam"
+
+ modified_obj.spam = "maybe_spam"
+ assert modified_obj.spam_ == "maybe_spam"
+
+ modified_obj.spam_ = "maybe_not_spam"
+ assert modified_obj.spam == "maybe_not_spam"
+
+ # Try nested values
+ modified_obj = TextStream(
+ text_stream=TestMessage({"spam": "has_spam", "eggs": "has_eggs"})
+ )
+ assert modified_obj.text_stream.spam_ == "has_spam"
+
+ # Test get and set for nested values
+ modified_obj.text_stream.spam = "no_spam"
+ assert modified_obj.text_stream.spam == "no_spam"
+
+ modified_obj.text_stream.spam_ = "yes_spam"
+ assert modified_obj.text_stream.spam_ == "yes_spam"
+
+ modified_obj.text_stream.spam = "maybe_spam"
+ assert modified_obj.text_stream.spam_ == "maybe_spam"
+
+ modified_obj.text_stream.spam_ = "maybe_not_spam"
+ assert modified_obj.text_stream.spam == "maybe_not_spam"
+
+ with pytest.raises(AttributeError):
+ assert modified_obj.text_stream.attribute_does_not_exist == "n/a"
+
+ with pytest.raises(AttributeError):
+ modified_obj.text_stream.attribute_does_not_exist = "n/a"
+
+ # Try using dict
+ modified_obj = TextStream(text_stream={"spam": "has_spam", "eggs": "has_eggs"})
+ assert modified_obj.text_stream.spam_ == "has_spam"
diff --git a/packages/proto-plus/tests/test_fields_oneof.py b/packages/proto-plus/tests/test_fields_oneof.py
new file mode 100644
index 000000000000..63a82764e79e
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_oneof.py
@@ -0,0 +1,49 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+
+
+def test_oneof():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1, oneof="bacon")
+ baz = proto.Field(proto.STRING, number=2, oneof="bacon")
+
+ foo = Foo(bar=42)
+ assert foo.bar == 42
+ assert not foo.baz
+ foo.baz = "the answer"
+ assert not foo.bar
+ assert foo.baz == "the answer"
+
+
+def test_multiple_oneofs():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1, oneof="spam")
+ baz = proto.Field(proto.STRING, number=2, oneof="spam")
+ bacon = proto.Field(proto.FLOAT, number=3, oneof="eggs")
+ ham = proto.Field(proto.STRING, number=4, oneof="eggs")
+
+ foo = Foo()
+ foo.bar = 42
+ foo.bacon = 42.0
+ assert foo.bar == 42
+ assert foo.bacon == 42.0
+ assert not foo.baz
+ assert not foo.ham
+ foo.ham = "this one gets assigned"
+ assert not foo.bacon
+ assert foo.ham == "this one gets assigned"
+ assert foo.bar == 42
+ assert not foo.baz
diff --git a/packages/proto-plus/tests/test_fields_optional.py b/packages/proto-plus/tests/test_fields_optional.py
new file mode 100644
index 000000000000..a15904c1a59d
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_optional.py
@@ -0,0 +1,87 @@
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+import proto
+
+
+def test_optional_init():
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1, optional=True)
+
+ squid_1 = Squid(mass_kg=20)
+ squid_2 = Squid()
+
+ assert Squid.mass_kg in squid_1
+ assert squid_1.mass_kg == 20
+ assert not Squid.mass_kg in squid_2
+
+ squid_2.mass_kg = 30
+ assert squid_2.mass_kg == 30
+ assert Squid.mass_kg in squid_2
+
+ del squid_1.mass_kg
+ assert not Squid.mass_kg in squid_1
+
+ with pytest.raises(AttributeError):
+ Squid.shell
+
+
+def test_optional_and_oneof():
+ # This test is a defensive check that synthetic oneofs
+ # don't interfere with user defined oneofs.
+
+ # Oneof defined before an optional
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1, oneof="mass")
+ mass_lbs = proto.Field(proto.INT32, number=2, oneof="mass")
+
+ iridiphore_num = proto.Field(proto.INT32, number=3, optional=True)
+
+ s = Squid(mass_kg=20)
+ assert s.mass_kg == 20
+ assert not s.mass_lbs
+ assert not Squid.iridiphore_num in s
+
+ s.iridiphore_num = 600
+ assert s.mass_kg == 20
+ assert not s.mass_lbs
+ assert Squid.iridiphore_num in s
+
+ s = Squid(mass_lbs=40, iridiphore_num=600)
+ assert not s.mass_kg
+ assert s.mass_lbs == 40
+ assert s.iridiphore_num == 600
+
+ # Oneof defined after an optional
+ class Clam(proto.Message):
+ flute_radius = proto.Field(proto.INT32, number=1, optional=True)
+
+ mass_kg = proto.Field(proto.INT32, number=2, oneof="mass")
+ mass_lbs = proto.Field(proto.INT32, number=3, oneof="mass")
+
+ c = Clam(mass_kg=20)
+
+ assert c.mass_kg == 20
+ assert not c.mass_lbs
+ assert not Clam.flute_radius in c
+ c.flute_radius = 30
+ assert c.mass_kg == 20
+ assert not c.mass_lbs
+
+ c = Clam(mass_lbs=40, flute_radius=30)
+ assert c.mass_lbs == 40
+ assert not c.mass_kg
+ assert c.flute_radius == 30
diff --git a/packages/proto-plus/tests/test_fields_repeated_composite.py b/packages/proto-plus/tests/test_fields_repeated_composite.py
new file mode 100644
index 000000000000..7c9f73e4415e
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_repeated_composite.py
@@ -0,0 +1,289 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from datetime import datetime
+from datetime import timezone
+from enum import Enum
+
+import pytest
+
+from google.protobuf import timestamp_pb2
+
+import proto
+from proto.datetime_helpers import DatetimeWithNanoseconds
+
+
+def test_repeated_composite_init():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz(foos=[Foo(bar=42)])
+ assert len(baz.foos) == 1
+ assert baz.foos == baz.foos
+ assert baz.foos[0].bar == 42
+ assert isinstance(baz.foos[0], Foo)
+
+
+def test_repeated_composite_equality():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz(foos=[Foo(bar=42)])
+ assert baz.foos == baz.foos
+ assert baz.foos != None
+
+
+def test_repeated_composite_init_struct():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz(foos=[{"bar": 42}])
+ assert len(baz.foos) == 1
+ assert baz.foos[0].bar == 42
+
+
+def test_repeated_composite_falsy_behavior():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz()
+ assert not baz.foos
+ assert len(baz.foos) == 0
+
+
+def test_repeated_composite_marshaled():
+ class Foo(proto.Message):
+ timestamps = proto.RepeatedField(
+ proto.MESSAGE,
+ message=timestamp_pb2.Timestamp,
+ number=1,
+ )
+
+ foo = Foo(
+ timestamps=[DatetimeWithNanoseconds(2012, 4, 21, 15, tzinfo=timezone.utc)]
+ )
+ foo.timestamps.append(timestamp_pb2.Timestamp(seconds=86400 * 365))
+ foo.timestamps.append(DatetimeWithNanoseconds(2017, 10, 14, tzinfo=timezone.utc))
+ assert all([isinstance(i, DatetimeWithNanoseconds) for i in foo.timestamps])
+ assert all([isinstance(i, timestamp_pb2.Timestamp) for i in Foo.pb(foo).timestamps])
+ assert foo.timestamps[0].year == 2012
+ assert foo.timestamps[0].month == 4
+ assert foo.timestamps[0].hour == 15
+ assert foo.timestamps[1].year == 1971
+ assert foo.timestamps[1].month == 1
+ assert foo.timestamps[1].hour == 0
+ assert foo.timestamps[2].year == 2017
+ assert foo.timestamps[2].month == 10
+ assert foo.timestamps[2].hour == 0
+
+
+def test_repeated_composite_enum():
+ class Foo(proto.Message):
+ class Bar(proto.Enum):
+ BAZ = 0
+
+ bars = proto.RepeatedField(Bar, number=1)
+
+ foo = Foo(bars=[Foo.Bar.BAZ])
+ assert isinstance(foo.bars[0], Enum)
+
+
+def test_repeated_composite_outer_write():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz()
+ baz.foos = [Foo(bar=96), Foo(bar=48)]
+ assert len(baz.foos) == 2
+ assert baz.foos[0].bar == 96
+ assert baz.foos[1].bar == 48
+
+
+def test_repeated_composite_append():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz()
+ baz.foos.append(Foo(bar=96))
+ baz.foos.append({"bar": 72})
+ assert len(baz.foos) == 2
+ assert baz.foos[0].bar == 96
+ assert baz.foos[1].bar == 72
+
+
+def test_repeated_composite_insert():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz()
+ baz.foos.insert(0, {"bar": 72})
+ baz.foos.insert(0, Foo(bar=96))
+ assert len(baz.foos) == 2
+ assert baz.foos[0].bar == 96
+ assert baz.foos[1].bar == 72
+
+
+def test_repeated_composite_iadd():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz()
+ baz.foos += [Foo(bar=96), Foo(bar=48)]
+ assert len(baz.foos) == 2
+ assert baz.foos[0].bar == 96
+ assert baz.foos[1].bar == 48
+
+
+def test_repeated_composite_set_index():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz(foos=[{"bar": 96}, {"bar": 48}])
+ baz.foos[1] = Foo(bar=55)
+ assert baz.foos[0].bar == 96
+ assert baz.foos[1].bar == 55
+ assert len(baz.foos) == 2
+
+
+def test_repeated_composite_set_index_error():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz(foos=[])
+ with pytest.raises(IndexError):
+ baz.foos[0] = Foo(bar=55)
+
+
+def test_repeated_composite_set_slice_less():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz(foos=[{"bar": 96}, {"bar": 48}, {"bar": 24}])
+ baz.foos[:2] = [{"bar": 12}]
+ assert baz.foos[0].bar == 12
+ assert baz.foos[1].bar == 24
+ assert len(baz.foos) == 2
+
+
+def test_repeated_composite_set_slice_more():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz(foos=[{"bar": 12}])
+ baz.foos[:2] = [{"bar": 96}, {"bar": 48}, {"bar": 24}]
+ assert baz.foos[0].bar == 96
+ assert baz.foos[1].bar == 48
+ assert baz.foos[2].bar == 24
+ assert len(baz.foos) == 3
+
+
+def test_repeated_composite_set_slice_not_iterable():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz(foos=[])
+ with pytest.raises(TypeError):
+ baz.foos[:1] = None
+
+
+def test_repeated_composite_set_extended_slice():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz(foos=[{"bar": 96}, {"bar": 48}])
+ baz.foos[::-1] = [{"bar": 96}, {"bar": 48}]
+ assert baz.foos[0].bar == 48
+ assert baz.foos[1].bar == 96
+ assert len(baz.foos) == 2
+
+
+def test_repeated_composite_set_extended_slice_wrong_length():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz(foos=[{"bar": 96}])
+ with pytest.raises(ValueError):
+ baz.foos[::-1] = []
+
+
+def test_repeated_composite_set_wrong_key_type():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz(foos=[])
+ with pytest.raises(TypeError):
+ baz.foos[None] = Foo(bar=55)
+
+
+def test_repeated_composite_set_wrong_value_type():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class NotFoo(proto.Message):
+ eggs = proto.Field(proto.INT32, number=1)
+
+ class Baz(proto.Message):
+ foos = proto.RepeatedField(proto.MESSAGE, message=Foo, number=1)
+
+ baz = Baz()
+ with pytest.raises(TypeError):
+ baz.foos.append(NotFoo(eggs=42))
diff --git a/packages/proto-plus/tests/test_fields_repeated_scalar.py b/packages/proto-plus/tests/test_fields_repeated_scalar.py
new file mode 100644
index 000000000000..d63831322e11
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_repeated_scalar.py
@@ -0,0 +1,115 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import copy
+
+import pytest
+
+import proto
+
+
+def test_repeated_scalar_init():
+ class Foo(proto.Message):
+ bar = proto.RepeatedField(proto.INT32, number=1)
+
+ foo = Foo(bar=[1, 1, 2, 3, 5, 8, 13])
+ assert foo.bar == [1, 1, 2, 3, 5, 8, 13]
+
+
+def test_repeated_scalar_append():
+ class Foo(proto.Message):
+ bar = proto.RepeatedField(proto.INT32, number=1)
+
+ foo = Foo(bar=[1, 1, 2, 3, 5, 8, 13])
+ foo.bar.append(21)
+ foo.bar.append(34)
+ assert foo.bar == [1, 1, 2, 3, 5, 8, 13, 21, 34]
+
+
+def test_repeated_scalar_iadd():
+ class Foo(proto.Message):
+ bar = proto.RepeatedField(proto.INT32, number=1)
+
+ foo = Foo(bar=[1, 1, 2, 3, 5, 8, 13])
+ foo.bar += [21, 34]
+ assert foo.bar == [1, 1, 2, 3, 5, 8, 13, 21, 34]
+
+
+def test_repeated_scalar_setitem():
+ class Foo(proto.Message):
+ bar = proto.RepeatedField(proto.INT32, number=1)
+
+ foo = Foo(bar=[1, 1, 2, 3, 5, 8, 13])
+ foo.bar[4] = 99
+ assert foo.bar == [1, 1, 2, 3, 99, 8, 13]
+ assert foo.bar[4] == 99
+
+
+def test_repeated_scalar_overwrite():
+ class Foo(proto.Message):
+ bar = proto.RepeatedField(proto.INT32, number=1)
+
+ foo = Foo(bar=[1, 1, 2, 3, 5, 8, 13])
+ foo.bar = [1, 2, 4, 8, 16]
+ assert foo.bar == [1, 2, 4, 8, 16]
+
+
+def test_repeated_scalar_eq_ne():
+ class Foo(proto.Message):
+ bar = proto.RepeatedField(proto.INT32, number=1)
+
+ foo = Foo(bar=[1, 1, 2, 3, 5, 8, 13])
+ assert foo.bar == copy.copy(foo.bar)
+ assert foo.bar != [1, 2, 4, 8, 16]
+ assert foo.bar != None
+
+
+def test_repeated_scalar_del():
+ class Foo(proto.Message):
+ bar = proto.RepeatedField(proto.INT32, number=1)
+
+ foo = Foo(bar=[1, 1, 2, 3, 5, 8, 13])
+ del foo.bar
+ assert foo.bar == []
+ assert not foo.bar
+
+
+def test_repeated_scalar_delitem():
+ class Foo(proto.Message):
+ bar = proto.RepeatedField(proto.INT32, number=1)
+
+ foo = Foo(bar=[1, 1, 2, 3, 5, 8, 13])
+ del foo.bar[5]
+ del foo.bar[3]
+ assert foo.bar == [1, 1, 2, 5, 13]
+
+
+def test_repeated_scalar_sort():
+ class Foo(proto.Message):
+ bar = proto.RepeatedField(proto.INT32, number=1)
+
+ foo = Foo(bar=[8, 1, 2, 1, 21, 3, 13, 5])
+ foo.bar.sort()
+ assert foo.bar == [1, 1, 2, 3, 5, 8, 13, 21]
+
+
+def test_repeated_scalar_wrong_type():
+ class Foo(proto.Message):
+ bar = proto.RepeatedField(proto.INT32, number=1)
+
+ foo = Foo(bar=[1, 1, 2, 3, 5, 8, 13])
+ with pytest.raises(TypeError):
+ foo.bar.append(21.0)
+ with pytest.raises(TypeError):
+ foo.bar.append("21")
diff --git a/packages/proto-plus/tests/test_fields_string.py b/packages/proto-plus/tests/test_fields_string.py
new file mode 100644
index 000000000000..6fd15c7311da
--- /dev/null
+++ b/packages/proto-plus/tests/test_fields_string.py
@@ -0,0 +1,57 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+
+
+def test_string_init():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.STRING, number=1)
+ baz = proto.Field(proto.STRING, number=2)
+
+ foo = Foo(bar="spam")
+ assert foo.bar == "spam"
+ assert foo.baz == ""
+ assert not foo.baz
+ assert Foo.pb(foo).bar == "spam"
+ assert Foo.pb(foo).baz == ""
+
+
+def test_string_rmw():
+ class Foo(proto.Message):
+ spam = proto.Field(proto.STRING, number=1)
+ eggs = proto.Field(proto.STRING, number=2)
+
+ foo = Foo(spam="bar")
+ foo.eggs = "baz"
+ assert foo.spam == "bar"
+ assert foo.eggs == "baz"
+ assert Foo.pb(foo).spam == "bar"
+ assert Foo.pb(foo).eggs == "baz"
+ foo.spam = "bacon"
+ assert foo.spam == "bacon"
+ assert foo.eggs == "baz"
+ assert Foo.pb(foo).spam == "bacon"
+ assert Foo.pb(foo).eggs == "baz"
+
+
+def test_string_del():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.STRING, number=1)
+
+ foo = Foo(bar="spam")
+ assert foo.bar == "spam"
+ del foo.bar
+ assert foo.bar == ""
+ assert not foo.bar
diff --git a/packages/proto-plus/tests/test_file_info_salting.py b/packages/proto-plus/tests/test_file_info_salting.py
new file mode 100644
index 000000000000..4fce9105270e
--- /dev/null
+++ b/packages/proto-plus/tests/test_file_info_salting.py
@@ -0,0 +1,80 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import collections
+
+import proto
+from google.protobuf import descriptor_pb2
+
+from proto import _file_info, _package_info
+
+
+def sample_file_info(name):
+ filename = name + ".proto"
+
+ # Get the essential information about the proto package, and where
+ # this component belongs within the file.
+ package, marshal = _package_info.compile(name, {})
+
+ # Get or create the information about the file, including the
+ # descriptor to which the new message descriptor shall be added.
+ return _file_info._FileInfo.registry.setdefault(
+ filename,
+ _file_info._FileInfo(
+ descriptor=descriptor_pb2.FileDescriptorProto(
+ name=filename,
+ package=package,
+ syntax="proto3",
+ ),
+ enums=collections.OrderedDict(),
+ messages=collections.OrderedDict(),
+ name=filename,
+ nested={},
+ nested_enum={},
+ ),
+ )
+
+
+def test_fallback_salt_is_appended_to_filename():
+ # given
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ name = "my-fileinfo"
+ fallback_salt = "my-fallback_salt"
+ file_info = sample_file_info(name)
+
+ # when
+ file_info.generate_file_pb(new_class=Foo, fallback_salt=fallback_salt)
+
+ # then
+ assert file_info.descriptor.name == name + "_" + fallback_salt + ".proto"
+
+
+def test_none_fallback_salt_is_appended_to_filename_as_empty():
+ # given
+
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ name = "my-fileinfo"
+ none_fallback_salt = None
+ file_info = sample_file_info(name)
+
+ # when
+ file_info.generate_file_pb(new_class=Foo, fallback_salt=none_fallback_salt)
+
+ # then
+ assert file_info.descriptor.name == name + ".proto"
diff --git a/packages/proto-plus/tests/test_file_info_salting_with_manifest.py b/packages/proto-plus/tests/test_file_info_salting_with_manifest.py
new file mode 100644
index 000000000000..2d8f75eb48c7
--- /dev/null
+++ b/packages/proto-plus/tests/test_file_info_salting_with_manifest.py
@@ -0,0 +1,94 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import collections
+
+import proto
+from google.protobuf import descriptor_pb2
+
+from proto import _file_info, _package_info
+
+PACKAGE = "a.test.package.salting.with.manifest"
+__protobuf__ = proto.module(
+ package=PACKAGE,
+ manifest={"This", "That"},
+)
+
+
+class This(proto.Message):
+ this = proto.Field(proto.INT32, number=1)
+
+
+class That(proto.Message):
+ that = proto.Field(proto.INT32, number=1)
+
+
+class NotInManifest(proto.Message):
+ them = proto.Field(proto.INT32, number=1)
+
+
+def sample_file_info(name):
+ filename = name + ".proto"
+
+ # Get the essential information about the proto package, and where
+ # this component belongs within the file.
+ package, marshal = _package_info.compile(name, {})
+
+ # Get or create the information about the file, including the
+ # descriptor to which the new message descriptor shall be added.
+ return _file_info._FileInfo.registry.setdefault(
+ filename,
+ _file_info._FileInfo(
+ descriptor=descriptor_pb2.FileDescriptorProto(
+ name=filename,
+ package=package,
+ syntax="proto3",
+ ),
+ enums=collections.OrderedDict(),
+ messages=collections.OrderedDict(),
+ name=filename,
+ nested={},
+ nested_enum={},
+ ),
+ )
+
+
+def test_no_salt_is_appended_to_filename_with_manifest():
+ # given
+ name = "my-filename"
+ fallback_salt = "my-fallback_salt"
+ file_info = sample_file_info(name)
+
+ # when
+ file_info.generate_file_pb(new_class=This, fallback_salt=fallback_salt)
+
+ # then
+ assert file_info.descriptor.name == name + ".proto"
+
+
+def test_none_fallback_salt_is_appended_to_filename_as_empty():
+ # given
+
+ name = "my-fileinfo"
+ none_fallback_salt = None
+ file_info = sample_file_info(name)
+
+ # when
+ file_info.generate_file_pb(
+ new_class=NotInManifest, fallback_salt=none_fallback_salt
+ )
+
+ # then
+ assert file_info.descriptor.name == name + ".proto"
diff --git a/packages/proto-plus/tests/test_json.py b/packages/proto-plus/tests/test_json.py
new file mode 100644
index 000000000000..ae3cf59eeb2e
--- /dev/null
+++ b/packages/proto-plus/tests/test_json.py
@@ -0,0 +1,265 @@
+# Copyright (C) 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+import re
+
+import proto
+from google.protobuf.json_format import MessageToJson, Parse, ParseError
+
+
+def test_message_to_json():
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+
+ s = Squid(mass_kg=100)
+ json = Squid.to_json(s)
+ json = json.replace(" ", "").replace("\n", "")
+ assert json == '{"massKg":100}'
+
+
+def test_message_to_json_no_indent():
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+ name = proto.Field(proto.STRING, number=2)
+
+ s = Squid(mass_kg=100, name="no_new_lines_squid")
+ json = Squid.to_json(s, indent=None)
+ assert json == '{"massKg": 100, "name": "no_new_lines_squid"}'
+
+
+def test_message_from_json():
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+
+ json = """{
+ "massKg": 100
+ }
+ """
+
+ s = Squid.from_json(json)
+ assert s == Squid(mass_kg=100)
+
+
+def test_message_json_round_trip():
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+
+ s = Squid(mass_kg=100)
+ json = Squid.to_json(s)
+ s2 = Squid.from_json(json)
+
+ assert s == s2
+
+
+def test_json_stringy_enums():
+ class Squid(proto.Message):
+ zone = proto.Field(proto.ENUM, number=1, enum="Zone")
+
+ class Zone(proto.Enum):
+ EPIPELAGIC = 0
+ MESOPELAGIC = 1
+ BATHYPELAGIC = 2
+ ABYSSOPELAGIC = 3
+
+ s1 = Squid(zone=Zone.MESOPELAGIC)
+ json = (
+ Squid.to_json(s1, use_integers_for_enums=False)
+ .replace(" ", "")
+ .replace("\n", "")
+ )
+ assert json == '{"zone":"MESOPELAGIC"}'
+
+ s2 = Squid.from_json(json)
+ assert s2.zone == s1.zone
+
+
+def test_json_default_enums():
+ class Squid(proto.Message):
+ zone = proto.Field(proto.ENUM, number=1, enum="Zone")
+
+ class Zone(proto.Enum):
+ EPIPELAGIC = 0
+ MESOPELAGIC = 1
+ BATHYPELAGIC = 2
+ ABYSSOPELAGIC = 3
+
+ s = Squid()
+ assert s.zone == Zone.EPIPELAGIC
+ json1 = Squid.to_json(s).replace(" ", "").replace("\n", "")
+ assert json1 == '{"zone":0}'
+
+ json2 = (
+ Squid.to_json(s, use_integers_for_enums=False)
+ .replace(" ", "")
+ .replace("\n", "")
+ )
+ assert json2 == '{"zone":"EPIPELAGIC"}'
+
+
+def test_json_default_values():
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+ name = proto.Field(proto.STRING, number=2)
+
+ s = Squid(name="Steve")
+ json1 = (
+ Squid.to_json(s, including_default_value_fields=False)
+ .replace(" ", "")
+ .replace("\n", "")
+ )
+ assert json1 == '{"name":"Steve"}'
+
+ json1 = (
+ Squid.to_json(s, always_print_fields_with_no_presence=False)
+ .replace(" ", "")
+ .replace("\n", "")
+ )
+ assert json1 == '{"name":"Steve"}'
+
+ json1 = (
+ Squid.to_json(
+ s,
+ including_default_value_fields=False,
+ always_print_fields_with_no_presence=False,
+ )
+ .replace(" ", "")
+ .replace("\n", "")
+ )
+ assert json1 == '{"name":"Steve"}'
+
+ with pytest.raises(
+ ValueError,
+ match="Arguments.*always_print_fields_with_no_presence.*including_default_value_fields.*must match",
+ ):
+ Squid.to_json(
+ s,
+ including_default_value_fields=True,
+ always_print_fields_with_no_presence=False,
+ ).replace(" ", "").replace("\n", "")
+
+ with pytest.raises(
+ ValueError,
+ match="Arguments.*always_print_fields_with_no_presence.*including_default_value_fields.*must match",
+ ):
+ Squid.to_json(
+ s,
+ including_default_value_fields=False,
+ always_print_fields_with_no_presence=True,
+ ).replace(" ", "").replace("\n", "")
+
+ json2 = (
+ Squid.to_json(
+ s,
+ including_default_value_fields=True,
+ always_print_fields_with_no_presence=True,
+ )
+ .replace(" ", "")
+ .replace("\n", "")
+ )
+ assert json2 == '{"name":"Steve","massKg":0}'
+
+ json2 = (
+ Squid.to_json(
+ s,
+ including_default_value_fields=True,
+ )
+ .replace(" ", "")
+ .replace("\n", "")
+ )
+ assert json2 == '{"name":"Steve","massKg":0}'
+
+ json2 = (
+ Squid.to_json(
+ s,
+ always_print_fields_with_no_presence=True,
+ )
+ .replace(" ", "")
+ .replace("\n", "")
+ )
+ assert json2 == '{"name":"Steve","massKg":0}'
+
+ json2 = Squid.to_json(s).replace(" ", "").replace("\n", "")
+ assert json2 == '{"name":"Steve","massKg":0}'
+
+ s1 = Squid.from_json(json1)
+ s2 = Squid.from_json(json2)
+ assert s == s1 == s2
+
+
+def test_json_unknown_field():
+ # Note that 'lengthCm' is unknown in the local definition.
+ # This could happen if the client is using an older proto definition
+ # than the server.
+ json_str = '{\n "massKg": 20,\n "lengthCm": 100\n}'
+
+ class Octopus(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+
+ o = Octopus.from_json(json_str, ignore_unknown_fields=True)
+ assert not hasattr(o, "length_cm")
+ assert not hasattr(o, "lengthCm")
+
+ # Don't permit unknown fields by default
+ with pytest.raises(ParseError):
+ o = Octopus.from_json(json_str)
+
+
+def test_json_snake_case():
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+
+ json_str = '{\n "mass_kg": 20\n}'
+ s = Squid.from_json(json_str)
+
+ assert s.mass_kg == 20
+
+ assert Squid.to_json(s, preserving_proto_field_name=True) == json_str
+
+
+def test_json_name():
+ class Squid(proto.Message):
+ massKg = proto.Field(proto.INT32, number=1, json_name="mass_in_kilograms")
+
+ s = Squid(massKg=20)
+ j = Squid.to_json(s)
+
+ assert "mass_in_kilograms" in j
+
+ s_two = Squid.from_json(j)
+
+ assert s == s_two
+
+
+def test_json_sort_keys():
+ class Squid(proto.Message):
+ name = proto.Field(proto.STRING, number=1)
+ mass_kg = proto.Field(proto.INT32, number=2)
+
+ s = Squid(name="Steve", mass_kg=20)
+ j = Squid.to_json(s, sort_keys=True, indent=None)
+
+ assert re.search(r"massKg.*name", j)
+
+
+# TODO: https://github.com/googleapis/proto-plus-python/issues/390
+def test_json_float_precision():
+ class Squid(proto.Message):
+ name = proto.Field(proto.STRING, number=1)
+ mass_kg = proto.Field(proto.FLOAT, number=2)
+
+ s = Squid(name="Steve", mass_kg=3.14159265)
+ j = Squid.to_json(s, float_precision=3, indent=None)
+
+ assert j == '{"name": "Steve", "massKg": 3.14}'
diff --git a/packages/proto-plus/tests/test_marshal_field_mask.py b/packages/proto-plus/tests/test_marshal_field_mask.py
new file mode 100644
index 000000000000..ffb36a2ff7a6
--- /dev/null
+++ b/packages/proto-plus/tests/test_marshal_field_mask.py
@@ -0,0 +1,100 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from google.protobuf import field_mask_pb2
+
+import proto
+from proto.marshal.marshal import BaseMarshal
+
+
+def test_field_mask_read():
+ class Foo(proto.Message):
+ mask = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=field_mask_pb2.FieldMask,
+ )
+
+ foo = Foo(mask=field_mask_pb2.FieldMask(paths=["f.b.d", "f.c"]))
+
+ assert isinstance(foo.mask, field_mask_pb2.FieldMask)
+ assert foo.mask.paths == ["f.b.d", "f.c"]
+
+
+def test_field_mask_write_string():
+ class Foo(proto.Message):
+ mask = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=field_mask_pb2.FieldMask,
+ )
+
+ foo = Foo()
+ foo.mask = "f.b.d,f.c"
+
+ assert isinstance(foo.mask, field_mask_pb2.FieldMask)
+ assert foo.mask.paths == ["f.b.d", "f.c"]
+
+
+def test_field_mask_write_pb2():
+ class Foo(proto.Message):
+ mask = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=field_mask_pb2.FieldMask,
+ )
+
+ foo = Foo()
+ foo.mask = field_mask_pb2.FieldMask(paths=["f.b.d", "f.c"])
+
+ assert isinstance(foo.mask, field_mask_pb2.FieldMask)
+ assert foo.mask.paths == ["f.b.d", "f.c"]
+
+
+def test_field_mask_absence():
+ class Foo(proto.Message):
+ mask = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=field_mask_pb2.FieldMask,
+ )
+
+ foo = Foo()
+ assert not foo.mask.paths
+
+
+def test_timestamp_del():
+ class Foo(proto.Message):
+ mask = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=field_mask_pb2.FieldMask,
+ )
+
+ foo = Foo()
+ foo.mask = field_mask_pb2.FieldMask(paths=["f.b.d", "f.c"])
+
+ del foo.mask
+ assert not foo.mask.paths
+
+
+def test_timestamp_to_python_idempotent():
+ # This path can never run in the current configuration because proto
+ # values are the only thing ever saved, and `to_python` is a read method.
+ #
+ # However, we test idempotency for consistency with `to_proto` and
+ # general resiliency.
+ marshal = BaseMarshal()
+ py_value = "f.b.d,f.c"
+ assert marshal.to_python(field_mask_pb2.FieldMask, py_value) is py_value
diff --git a/packages/proto-plus/tests/test_marshal_register.py b/packages/proto-plus/tests/test_marshal_register.py
new file mode 100644
index 000000000000..3ca1a2a88d73
--- /dev/null
+++ b/packages/proto-plus/tests/test_marshal_register.py
@@ -0,0 +1,48 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from google.protobuf import empty_pb2
+
+from proto.marshal.marshal import BaseMarshal
+
+
+def test_registration():
+ marshal = BaseMarshal()
+
+ @marshal.register(empty_pb2.Empty)
+ class Rule:
+ def to_proto(self, value):
+ return value
+
+ def to_python(self, value, *, absent=None):
+ return value
+
+ assert isinstance(marshal._rules[empty_pb2.Empty], Rule)
+
+
+def test_invalid_marshal_class():
+ marshal = BaseMarshal()
+ with pytest.raises(TypeError):
+
+ @marshal.register(empty_pb2.Empty)
+ class Marshal:
+ pass
+
+
+def test_invalid_marshal_rule():
+ marshal = BaseMarshal()
+ with pytest.raises(TypeError):
+ marshal.register(empty_pb2.Empty, rule=object())
diff --git a/packages/proto-plus/tests/test_marshal_strict.py b/packages/proto-plus/tests/test_marshal_strict.py
new file mode 100644
index 000000000000..75e2f05de90b
--- /dev/null
+++ b/packages/proto-plus/tests/test_marshal_strict.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+from proto.marshal.marshal import BaseMarshal
+import pytest
+
+
+def test_strict_to_proto():
+ m = BaseMarshal()
+
+ with pytest.raises(TypeError):
+ m.to_proto(dict, None, strict=True)
diff --git a/packages/proto-plus/tests/test_marshal_stringy_numbers.py b/packages/proto-plus/tests/test_marshal_stringy_numbers.py
new file mode 100644
index 000000000000..f3f40831dae6
--- /dev/null
+++ b/packages/proto-plus/tests/test_marshal_stringy_numbers.py
@@ -0,0 +1,50 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from proto.marshal.marshal import BaseMarshal
+from proto.primitives import ProtoType
+
+INT_32BIT_PLUS_ONE = 0xFFFFFFFF + 1
+
+
+@pytest.mark.parametrize(
+ "pb_type,value,expected",
+ [
+ (ProtoType.INT64, 0, 0),
+ (ProtoType.INT64, INT_32BIT_PLUS_ONE, INT_32BIT_PLUS_ONE),
+ (ProtoType.SINT64, -INT_32BIT_PLUS_ONE, -INT_32BIT_PLUS_ONE),
+ (ProtoType.INT64, None, None),
+ (ProtoType.UINT64, 0, 0),
+ (ProtoType.UINT64, INT_32BIT_PLUS_ONE, INT_32BIT_PLUS_ONE),
+ (ProtoType.UINT64, None, None),
+ (ProtoType.SINT64, 0, 0),
+ (ProtoType.SINT64, INT_32BIT_PLUS_ONE, INT_32BIT_PLUS_ONE),
+ (ProtoType.SINT64, -INT_32BIT_PLUS_ONE, -INT_32BIT_PLUS_ONE),
+ (ProtoType.SINT64, None, None),
+ (ProtoType.FIXED64, 0, 0),
+ (ProtoType.FIXED64, INT_32BIT_PLUS_ONE, INT_32BIT_PLUS_ONE),
+ (ProtoType.FIXED64, -INT_32BIT_PLUS_ONE, -INT_32BIT_PLUS_ONE),
+ (ProtoType.FIXED64, None, None),
+ (ProtoType.SFIXED64, 0, 0),
+ (ProtoType.SFIXED64, INT_32BIT_PLUS_ONE, INT_32BIT_PLUS_ONE),
+ (ProtoType.SFIXED64, -INT_32BIT_PLUS_ONE, -INT_32BIT_PLUS_ONE),
+ (ProtoType.SFIXED64, None, None),
+ ],
+)
+def test_marshal_to_proto_stringy_numbers(pb_type, value, expected):
+
+ marshal = BaseMarshal()
+ assert marshal.to_proto(pb_type, value) == expected
diff --git a/packages/proto-plus/tests/test_marshal_types_dates.py b/packages/proto-plus/tests/test_marshal_types_dates.py
new file mode 100644
index 000000000000..44c65ebc778d
--- /dev/null
+++ b/packages/proto-plus/tests/test_marshal_types_dates.py
@@ -0,0 +1,327 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from datetime import datetime
+from datetime import timedelta
+from datetime import timezone
+
+from google.protobuf import duration_pb2
+from google.protobuf import timestamp_pb2
+
+import proto
+from proto.marshal.marshal import BaseMarshal
+from proto import datetime_helpers
+from proto.datetime_helpers import DatetimeWithNanoseconds
+
+
+def test_timestamp_read():
+ class Foo(proto.Message):
+ event_time = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=timestamp_pb2.Timestamp,
+ )
+
+ foo = Foo(event_time=timestamp_pb2.Timestamp(seconds=1335020400))
+
+ assert isinstance(foo.event_time, DatetimeWithNanoseconds)
+ assert foo.event_time.year == 2012
+ assert foo.event_time.month == 4
+ assert foo.event_time.day == 21
+ assert foo.event_time.hour == 15
+ assert foo.event_time.minute == 0
+ assert foo.event_time.tzinfo == timezone.utc
+
+
+def test_timestamp_write_init():
+ class Foo(proto.Message):
+ event_time = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=timestamp_pb2.Timestamp,
+ )
+
+ foo = Foo(event_time=DatetimeWithNanoseconds(2012, 4, 21, 15, tzinfo=timezone.utc))
+ assert isinstance(foo.event_time, DatetimeWithNanoseconds)
+ assert isinstance(Foo.pb(foo).event_time, timestamp_pb2.Timestamp)
+ assert foo.event_time.year == 2012
+ assert foo.event_time.month == 4
+ assert foo.event_time.hour == 15
+ assert Foo.pb(foo).event_time.seconds == 1335020400
+
+
+def test_timestamp_write():
+ class Foo(proto.Message):
+ event_time = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=timestamp_pb2.Timestamp,
+ )
+
+ foo = Foo()
+ dns = DatetimeWithNanoseconds(2012, 4, 21, 15, tzinfo=timezone.utc)
+ foo.event_time = dns
+ assert isinstance(foo.event_time, DatetimeWithNanoseconds)
+ assert isinstance(Foo.pb(foo).event_time, timestamp_pb2.Timestamp)
+ assert foo.event_time.year == 2012
+ assert foo.event_time.month == 4
+ assert foo.event_time.hour == 15
+ assert Foo.pb(foo).event_time.seconds == 1335020400
+
+
+def test_timestamp_write_pb2():
+ class Foo(proto.Message):
+ event_time = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=timestamp_pb2.Timestamp,
+ )
+
+ foo = Foo()
+ foo.event_time = timestamp_pb2.Timestamp(seconds=1335020400)
+ assert isinstance(foo.event_time, DatetimeWithNanoseconds)
+ assert isinstance(Foo.pb(foo).event_time, timestamp_pb2.Timestamp)
+ assert foo.event_time.year == 2012
+ assert foo.event_time.month == 4
+ assert foo.event_time.hour == 15
+ assert Foo.pb(foo).event_time.seconds == 1335020400
+
+
+def test_timestamp_write_string():
+ class Foo(proto.Message):
+ event_time = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=timestamp_pb2.Timestamp,
+ )
+
+ foo = Foo()
+ foo.event_time = "2012-04-21T15:00:00Z"
+ assert isinstance(foo.event_time, DatetimeWithNanoseconds)
+ assert isinstance(Foo.pb(foo).event_time, timestamp_pb2.Timestamp)
+ assert foo.event_time.year == 2012
+ assert foo.event_time.month == 4
+ assert foo.event_time.hour == 15
+ assert Foo.pb(foo).event_time.seconds == 1335020400
+
+
+def test_timestamp_rmw_nanos():
+ class Foo(proto.Message):
+ event_time = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=timestamp_pb2.Timestamp,
+ )
+
+ foo = Foo()
+ foo.event_time = DatetimeWithNanoseconds(
+ 2012, 4, 21, 15, 0, 0, 1, tzinfo=timezone.utc
+ )
+ assert foo.event_time.microsecond == 1
+ assert Foo.pb(foo).event_time.nanos == 1000
+ foo.event_time = foo.event_time.replace(microsecond=2)
+ assert foo.event_time.microsecond == 2
+ assert Foo.pb(foo).event_time.nanos == 2000
+
+
+def test_timestamp_absence():
+ class Foo(proto.Message):
+ event_time = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=timestamp_pb2.Timestamp,
+ )
+
+ foo = Foo()
+ assert foo.event_time is None
+
+
+def test_timestamp_del():
+ class Foo(proto.Message):
+ event_time = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=timestamp_pb2.Timestamp,
+ )
+
+ foo = Foo(event_time=DatetimeWithNanoseconds(2012, 4, 21, 15, tzinfo=timezone.utc))
+ del foo.event_time
+ assert foo.event_time is None
+
+
+def test_duration_read():
+ class Foo(proto.Message):
+ ttl = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=duration_pb2.Duration,
+ )
+
+ foo = Foo(ttl=duration_pb2.Duration(seconds=60, nanos=1000))
+ assert isinstance(foo.ttl, timedelta)
+ assert isinstance(Foo.pb(foo).ttl, duration_pb2.Duration)
+ assert foo.ttl.days == 0
+ assert foo.ttl.seconds == 60
+ assert foo.ttl.microseconds == 1
+
+
+def test_duration_write_init():
+ class Foo(proto.Message):
+ ttl = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=duration_pb2.Duration,
+ )
+
+ foo = Foo(ttl=timedelta(days=2))
+ assert isinstance(foo.ttl, timedelta)
+ assert isinstance(Foo.pb(foo).ttl, duration_pb2.Duration)
+ assert foo.ttl.days == 2
+ assert foo.ttl.seconds == 0
+ assert foo.ttl.microseconds == 0
+ assert Foo.pb(foo).ttl.seconds == 86400 * 2
+
+
+def test_duration_write():
+ class Foo(proto.Message):
+ ttl = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=duration_pb2.Duration,
+ )
+
+ foo = Foo()
+ foo.ttl = timedelta(seconds=120)
+ assert isinstance(foo.ttl, timedelta)
+ assert isinstance(Foo.pb(foo).ttl, duration_pb2.Duration)
+ assert foo.ttl.seconds == 120
+ assert Foo.pb(foo).ttl.seconds == 120
+
+
+def test_duration_write_pb2():
+ class Foo(proto.Message):
+ ttl = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=duration_pb2.Duration,
+ )
+
+ foo = Foo()
+ foo.ttl = duration_pb2.Duration(seconds=120)
+ assert isinstance(foo.ttl, timedelta)
+ assert isinstance(Foo.pb(foo).ttl, duration_pb2.Duration)
+ assert foo.ttl.seconds == 120
+ assert Foo.pb(foo).ttl.seconds == 120
+
+
+def test_duration_write_string():
+ class Foo(proto.Message):
+ ttl = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=duration_pb2.Duration,
+ )
+
+ foo = Foo()
+ foo.ttl = "120s"
+ assert isinstance(foo.ttl, timedelta)
+ assert isinstance(Foo.pb(foo).ttl, duration_pb2.Duration)
+ assert foo.ttl.seconds == 120
+ assert Foo.pb(foo).ttl.seconds == 120
+
+
+def test_duration_write_string_nested():
+ class Foo(proto.Message):
+ foo_field: duration_pb2.Duration = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=duration_pb2.Duration,
+ )
+
+ Foo(foo_field="300s")
+
+ class Bar(proto.Message):
+ bar_field = proto.Field(proto.MESSAGE, number=1, message=Foo)
+
+ bar = Bar({"bar_field": {"foo_field": "300s"}})
+ assert isinstance(bar.bar_field.foo_field, timedelta)
+ assert isinstance(Bar.pb(bar).bar_field.foo_field, duration_pb2.Duration)
+ assert bar.bar_field.foo_field.seconds == 300
+ assert Bar.pb(bar).bar_field.foo_field.seconds == 300
+
+
+def test_duration_del():
+ class Foo(proto.Message):
+ ttl = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=duration_pb2.Duration,
+ )
+
+ foo = Foo(ttl=timedelta(seconds=900))
+ del foo.ttl
+ assert isinstance(foo.ttl, timedelta)
+ assert foo.ttl.days == 0
+ assert foo.ttl.seconds == 0
+ assert foo.ttl.microseconds == 0
+
+
+def test_duration_nanos_rmw():
+ class Foo(proto.Message):
+ ttl = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=duration_pb2.Duration,
+ )
+
+ foo = Foo(ttl=timedelta(microseconds=50))
+ assert foo.ttl.microseconds == 50
+ assert Foo.pb(foo).ttl.nanos == 50000
+ foo.ttl = timedelta(microseconds=25)
+ assert Foo.pb(foo).ttl.nanos == 25000
+ assert foo.ttl.microseconds == 25
+
+
+def test_timestamp_to_python_idempotent():
+ # This path can never run in the current configuration because proto
+ # values are the only thing ever saved, and `to_python` is a read method.
+ #
+ # However, we test idempotency for consistency with `to_proto` and
+ # general resiliency.
+ marshal = BaseMarshal()
+ py_value = DatetimeWithNanoseconds(2012, 4, 21, 15, tzinfo=timezone.utc)
+ assert marshal.to_python(timestamp_pb2.Timestamp, py_value) is py_value
+
+
+def test_duration_to_python_idempotent():
+ # This path can never run in the current configuration because proto
+ # values are the only thing ever saved, and `to_python` is a read method.
+ #
+ # However, we test idempotency for consistency with `to_proto` and
+ # general resiliency.
+ marshal = BaseMarshal()
+ py_value = timedelta(seconds=240)
+ assert marshal.to_python(duration_pb2.Duration, py_value) is py_value
+
+
+def test_vanilla_datetime_construction():
+ # 99% of users are going to want to pass in regular datetime objects.
+ # Make sure that this interoperates well with nanosecond precision.
+ class User(proto.Message):
+ birthday = proto.Field(timestamp_pb2.Timestamp, number=1)
+
+ # Our user WAs born yesterday.
+ bday = datetime.now(tz=timezone.utc) + timedelta(days=-1)
+ u = User(birthday=bday)
+ assert u.birthday == bday
diff --git a/packages/proto-plus/tests/test_marshal_types_enum.py b/packages/proto-plus/tests/test_marshal_types_enum.py
new file mode 100644
index 000000000000..acb554618dfa
--- /dev/null
+++ b/packages/proto-plus/tests/test_marshal_types_enum.py
@@ -0,0 +1,100 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from unittest import mock
+import warnings
+
+import proto
+from proto.marshal.rules.enums import EnumRule
+
+__protobuf__ = proto.module(package="test.marshal.enum")
+
+
+def test_to_proto():
+ class Foo(proto.Enum):
+ FOO_UNSPECIFIED = 0
+ BAR = 1
+ BAZ = 2
+
+ enum_rule = EnumRule(Foo)
+ foo_a = enum_rule.to_proto(Foo.BAR)
+ foo_b = enum_rule.to_proto(1)
+ foo_c = enum_rule.to_proto("BAR")
+ # We want to distinguish literal `1` from `Foo.BAR` here
+ # (they are equivalent but not identical).
+ assert foo_a is foo_b is foo_c is 1 # noqa: F632
+
+
+def test_to_python():
+ class Foo(proto.Enum):
+ FOO_UNSPECIFIED = 0
+ BAR = 1
+ BAZ = 2
+
+ enum_rule = EnumRule(Foo)
+ foo_a = enum_rule.to_python(1)
+ foo_b = enum_rule.to_python(Foo.BAR)
+ assert foo_a is foo_b is Foo.BAR
+
+
+def test_to_python_unknown_value():
+ class Foo(proto.Enum):
+ FOO_UNSPECIFIED = 0
+ BAR = 1
+ BAZ = 2
+
+ enum_rule = EnumRule(Foo)
+ with mock.patch.object(warnings, "warn") as warn:
+ assert enum_rule.to_python(4) == 4
+ warn.assert_called_once_with("Unrecognized Foo enum value: 4")
+
+
+def test_enum_append():
+ class Bivalve(proto.Enum):
+ CLAM = 0
+ OYSTER = 1
+
+ class MolluscContainer(proto.Message):
+ bivalves = proto.RepeatedField(
+ proto.ENUM,
+ number=1,
+ enum=Bivalve,
+ )
+
+ mc = MolluscContainer()
+ clam = Bivalve.CLAM
+ mc.bivalves.append(clam)
+ mc.bivalves.append(1)
+
+ assert mc.bivalves == [clam, Bivalve.OYSTER]
+
+
+def test_enum_map_insert():
+ class Bivalve(proto.Enum):
+ CLAM = 0
+ OYSTER = 1
+
+ class MolluscContainer(proto.Message):
+ bivalves = proto.MapField(
+ proto.STRING,
+ proto.ENUM,
+ number=1,
+ enum=Bivalve,
+ )
+
+ mc = MolluscContainer()
+ clam = Bivalve.CLAM
+ mc.bivalves["clam"] = clam
+ mc.bivalves["oyster"] = 1
+ assert dict(mc.bivalves) == {"clam": clam, "oyster": Bivalve.OYSTER}
diff --git a/packages/proto-plus/tests/test_marshal_types_message.py b/packages/proto-plus/tests/test_marshal_types_message.py
new file mode 100644
index 000000000000..b96ec32285d8
--- /dev/null
+++ b/packages/proto-plus/tests/test_marshal_types_message.py
@@ -0,0 +1,37 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+from proto.marshal.rules.message import MessageRule
+
+
+def test_to_proto():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ message_rule = MessageRule(Foo.pb(), Foo)
+ foo_pb2_a = message_rule.to_proto(Foo(bar=42))
+ foo_pb2_b = message_rule.to_proto(Foo.pb()(bar=42))
+ foo_pb2_c = message_rule.to_proto({"bar": 42})
+ assert foo_pb2_a == foo_pb2_b == foo_pb2_c
+
+
+def test_to_python():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ message_rule = MessageRule(Foo.pb(), Foo)
+ foo_a = message_rule.to_python(Foo(bar=42))
+ foo_b = message_rule.to_python(Foo.pb()(bar=42))
+ assert foo_a == foo_b
diff --git a/packages/proto-plus/tests/test_marshal_types_struct.py b/packages/proto-plus/tests/test_marshal_types_struct.py
new file mode 100644
index 000000000000..8ca2cde8a3c1
--- /dev/null
+++ b/packages/proto-plus/tests/test_marshal_types_struct.py
@@ -0,0 +1,268 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from google.protobuf import struct_pb2
+
+import proto
+
+
+def test_value_primitives_read():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ assert Foo(value=3).value == 3.0
+ assert Foo(value="3").value == "3"
+ assert Foo(value=None).value is None
+ assert Foo(value=False).value is False
+ assert Foo(value=True).value is True
+
+
+def test_value_absent():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ assert Foo().value is None
+
+
+def test_value_primitives_rmw():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ foo = Foo()
+ foo.value = 3
+ assert isinstance(foo.value, float)
+ assert abs(Foo.pb(foo).value.number_value - 3.0) < 1e-7
+ foo.value = False
+ assert not foo.value
+ assert foo.value is False
+ assert Foo.pb(foo).value.WhichOneof("kind") == "bool_value"
+ foo.value = None
+ assert not foo.value
+ assert foo.value is None
+ assert Foo.pb(foo).value.WhichOneof("kind") == "null_value"
+
+
+def test_value_write_pb():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ foo = Foo(value=struct_pb2.Value(string_value="stringy"))
+ assert foo.value == "stringy"
+
+
+def test_value_lists_read():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ foo = Foo(value=["3", None, "foo", True])
+ assert foo.value == ["3", None, "foo", True]
+
+
+def test_value_lists_null():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.ListValue, number=1)
+
+ foo = Foo()
+ assert foo.value is None
+
+
+def test_value_struct_null():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Struct, number=1)
+
+ foo = Foo()
+ assert foo.value is None
+
+
+def test_value_lists_rmw():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ foo = Foo(value=["3", None, "foo", True])
+ foo.value.append("bar")
+ foo.value.pop(1)
+ assert foo.value == ["3", "foo", True, "bar"]
+
+
+def test_value_lists_nested():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ foo = Foo(value=[[True, False], [True, False]])
+ foo.value.append([False, True])
+ assert foo.value == [[True, False], [True, False], [False, True]]
+
+
+def test_value_lists_struct():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ foo = Foo(value=[{"foo": "bar", "spam": "eggs"}])
+ assert foo.value == [{"foo": "bar", "spam": "eggs"}]
+
+
+def test_value_lists_detachment():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ foo = Foo(value=["foo", "bar"])
+ detached_list = foo.value
+ detached_list.append("baz")
+ assert foo.value == ["foo", "bar", "baz"]
+
+
+def test_value_structs_read():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ foo = Foo(value={"foo": True, "bar": False})
+ assert foo.value == {"foo": True, "bar": False}
+
+
+def test_value_structs_rmw():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ foo = Foo(value={"foo": True, "bar": False})
+ foo.value["baz"] = "a string"
+ assert foo.value == {"foo": True, "bar": False, "baz": "a string"}
+
+
+def test_value_structs_nested():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ foo = Foo(value={"foo": True, "bar": {"spam": "eggs"}})
+ assert foo.value == {"foo": True, "bar": {"spam": "eggs"}}
+ sv = Foo.pb(foo).value.struct_value
+ assert sv["foo"] is True
+ assert sv["bar"].fields["spam"].string_value == "eggs"
+
+
+def test_value_invalid_value():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ with pytest.raises(ValueError):
+ Foo(value=object())
+
+
+def test_value_unset():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Value, number=1)
+
+ foo = Foo()
+ assert "value" not in foo
+
+
+def test_list_value_read():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.ListValue, number=1)
+
+ foo = Foo(value=["foo", "bar", True, {"spam": "eggs"}])
+ assert foo.value == ["foo", "bar", True, {"spam": "eggs"}]
+
+
+def test_list_value_pb():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.ListValue, number=1)
+
+ foo = Foo(
+ value=struct_pb2.ListValue(
+ values=[
+ struct_pb2.Value(string_value="foo"),
+ struct_pb2.Value(string_value="bar"),
+ struct_pb2.Value(bool_value=True),
+ ]
+ )
+ )
+ assert foo.value == ["foo", "bar", True]
+
+
+def test_list_value_reassignment():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.ListValue, number=1)
+
+ foo = Foo(value=["foo", "bar"])
+ detached = foo.value
+ detached.append(True)
+ foo.value = detached
+ assert foo.value == ["foo", "bar", True]
+
+
+def test_list_value_invalid():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.ListValue, number=1)
+
+ with pytest.raises(TypeError):
+ Foo(value=3)
+
+
+def test_struct_read():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Struct, number=1)
+
+ foo = Foo(value={"foo": "bar", "bacon": True})
+ assert foo.value == {"foo": "bar", "bacon": True}
+
+
+def test_struct_pb():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Struct, number=1)
+
+ foo = Foo(
+ value=struct_pb2.Struct(
+ fields={
+ "foo": struct_pb2.Value(string_value="bar"),
+ "bacon": struct_pb2.Value(bool_value=True),
+ }
+ )
+ )
+ assert foo.value == {"foo": "bar", "bacon": True}
+
+
+def test_struct_reassignment():
+ class Foo(proto.Message):
+ value = proto.Field(struct_pb2.Struct, number=1)
+
+ foo = Foo(value={"foo": "bar"})
+ detached = foo.value
+ detached["bacon"] = True
+ foo.value = detached
+ assert foo.value == {"foo": "bar", "bacon": True}
+
+
+def test_struct_nested():
+ class Foo(proto.Message):
+ struct_field: struct_pb2.Struct = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=struct_pb2.Struct,
+ )
+
+ class Bar(proto.Message):
+ foo_field: Foo = proto.Field(
+ proto.MESSAGE,
+ number=1,
+ message=Foo,
+ )
+
+ foo = Foo({"struct_field": {"foo": "bagel"}})
+ assert foo.struct_field == {"foo": "bagel"}
+
+ bar = Bar({"foo_field": {"struct_field": {"foo": "cheese"}}})
+ assert bar.foo_field == Foo({"struct_field": {"foo": "cheese"}})
+ assert bar.foo_field.struct_field == {"foo": "cheese"}
diff --git a/packages/proto-plus/tests/test_marshal_types_wrappers_bool.py b/packages/proto-plus/tests/test_marshal_types_wrappers_bool.py
new file mode 100644
index 000000000000..e27caf990dcb
--- /dev/null
+++ b/packages/proto-plus/tests/test_marshal_types_wrappers_bool.py
@@ -0,0 +1,122 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from google.protobuf import wrappers_pb2
+
+import proto
+from proto.marshal.marshal import BaseMarshal
+
+
+def test_bool_value_init():
+ class Foo(proto.Message):
+ bar = proto.Field(
+ proto.MESSAGE,
+ message=wrappers_pb2.BoolValue,
+ number=1,
+ )
+
+ assert Foo(bar=True).bar is True
+ assert Foo(bar=False).bar is False
+ assert Foo().bar is None
+
+
+def test_bool_value_init_dict():
+ class Foo(proto.Message):
+ bar = proto.Field(
+ proto.MESSAGE,
+ message=wrappers_pb2.BoolValue,
+ number=1,
+ )
+
+ assert Foo({"bar": True}).bar is True
+ assert Foo({"bar": False}).bar is False
+ assert Foo({"bar": None}).bar is None
+
+
+def test_bool_value_distinction_from_bool():
+ class Foo(proto.Message):
+ bar = proto.Field(
+ proto.MESSAGE,
+ message=wrappers_pb2.BoolValue,
+ number=1,
+ )
+ baz = proto.Field(proto.BOOL, number=2)
+
+ assert Foo().bar is None
+ assert Foo().baz is False
+
+
+def test_bool_value_rmw():
+ class Foo(proto.Message):
+ bar = proto.Field(wrappers_pb2.BoolValue, number=1)
+ baz = proto.Field(wrappers_pb2.BoolValue, number=2)
+
+ foo = Foo(bar=False)
+ assert foo.bar is False
+ assert foo.baz is None
+ foo.baz = True
+ assert foo.baz is True
+ assert Foo.pb(foo).baz.value is True
+ foo.bar = None
+ assert foo.bar is None
+ assert not Foo.pb(foo).HasField("bar")
+
+
+def test_bool_value_write_bool_value():
+ class Foo(proto.Message):
+ bar = proto.Field(
+ proto.MESSAGE,
+ message=wrappers_pb2.BoolValue,
+ number=1,
+ )
+
+ foo = Foo(bar=True)
+ foo.bar = wrappers_pb2.BoolValue()
+ assert foo.bar is False
+
+
+def test_bool_value_del():
+ class Foo(proto.Message):
+ bar = proto.Field(
+ proto.MESSAGE,
+ message=wrappers_pb2.BoolValue,
+ number=1,
+ )
+
+ foo = Foo(bar=False)
+ assert foo.bar is False
+ del foo.bar
+ assert foo.bar is None
+
+
+def test_multiple_types():
+ class Foo(proto.Message):
+ bar = proto.Field(wrappers_pb2.BoolValue, number=1)
+ baz = proto.Field(wrappers_pb2.Int32Value, number=2)
+
+ foo = Foo(bar=True, baz=42)
+ assert foo.bar is True
+ assert foo.baz == 42
+
+
+def test_bool_value_to_python():
+ # This path can never run in the current configuration because proto
+ # values are the only thing ever saved, and `to_python` is a read method.
+ #
+ # However, we test idempotency for consistency with `to_proto` and
+ # general resiliency.
+ marshal = BaseMarshal()
+ assert marshal.to_python(wrappers_pb2.BoolValue, True) is True
+ assert marshal.to_python(wrappers_pb2.BoolValue, False) is False
+ assert marshal.to_python(wrappers_pb2.BoolValue, None) is None
diff --git a/packages/proto-plus/tests/test_message.py b/packages/proto-plus/tests/test_message.py
new file mode 100644
index 000000000000..720995a90787
--- /dev/null
+++ b/packages/proto-plus/tests/test_message.py
@@ -0,0 +1,487 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import itertools
+import pytest
+
+import proto
+
+
+def test_message_constructor_instance():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT64, number=1)
+
+ foo_original = Foo(bar=42)
+ foo_copy = Foo(foo_original)
+ assert foo_original.bar == foo_copy.bar == 42
+ assert foo_original == foo_copy
+ assert foo_original is not foo_copy
+ assert isinstance(foo_original, Foo)
+ assert isinstance(foo_copy, Foo)
+ assert isinstance(Foo.pb(foo_copy), Foo.pb())
+
+
+def test_message_constructor_underlying_pb2():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT64, number=1)
+
+ foo_pb2 = Foo.pb()(bar=42)
+ foo = Foo(foo_pb2)
+ assert foo.bar == Foo.pb(foo).bar == foo_pb2.bar == 42
+ assert foo == foo_pb2 # Not communitive. Nothing we can do about that.
+ assert foo_pb2 == Foo.pb(foo)
+ assert foo_pb2 is not Foo.pb(foo)
+ assert isinstance(foo, Foo)
+ assert isinstance(Foo.pb(foo), Foo.pb())
+ assert isinstance(foo_pb2, Foo.pb())
+
+
+def test_message_constructor_underlying_pb2_and_kwargs():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT64, number=1)
+
+ foo_pb2 = Foo.pb()(bar=42)
+ foo = Foo(foo_pb2, bar=99)
+ assert foo.bar == Foo.pb(foo).bar == 99
+ assert foo_pb2.bar == 42
+ assert isinstance(foo, Foo)
+ assert isinstance(Foo.pb(foo), Foo.pb())
+ assert isinstance(foo_pb2, Foo.pb())
+
+
+def test_message_constructor_dict():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT64, number=1)
+
+ foo = Foo({"bar": 42})
+ assert foo.bar == Foo.pb(foo).bar == 42
+ assert foo != {"bar": 42}
+ assert isinstance(foo, Foo)
+ assert isinstance(Foo.pb(foo), Foo.pb())
+
+
+def test_message_constructor_kwargs():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT64, number=1)
+
+ foo = Foo(bar=42)
+ assert foo.bar == Foo.pb(foo).bar == 42
+ assert isinstance(foo, Foo)
+ assert isinstance(Foo.pb(foo), Foo.pb())
+
+
+def test_message_constructor_invalid():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT64, number=1)
+
+ with pytest.raises(TypeError):
+ Foo(object())
+
+
+def test_message_constructor_explicit_qualname():
+ class Foo(proto.Message):
+ __qualname__ = "Foo"
+ bar = proto.Field(proto.INT64, number=1)
+
+ foo_original = Foo(bar=42)
+ foo_copy = Foo(foo_original)
+ assert foo_original.bar == foo_copy.bar == 42
+ assert foo_original == foo_copy
+ assert foo_original is not foo_copy
+ assert isinstance(foo_original, Foo)
+ assert isinstance(foo_copy, Foo)
+ assert isinstance(Foo.pb(foo_copy), Foo.pb())
+
+
+def test_message_contains_primitive():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT64, number=1)
+
+ assert "bar" in Foo(bar=42)
+ assert "bar" not in Foo(bar=0)
+ assert "bar" not in Foo()
+
+
+def test_message_contains_composite():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT64, number=1)
+
+ class Baz(proto.Message):
+ foo = proto.Field(proto.MESSAGE, number=1, message=Foo)
+
+ assert "foo" in Baz(foo=Foo(bar=42))
+ assert "foo" in Baz(foo=Foo())
+ assert "foo" not in Baz()
+
+
+def test_message_contains_repeated_primitive():
+ class Foo(proto.Message):
+ bar = proto.RepeatedField(proto.INT64, number=1)
+
+ assert "bar" in Foo(bar=[1, 1, 2, 3, 5])
+ assert "bar" in Foo(bar=[0])
+ assert "bar" not in Foo(bar=[])
+ assert "bar" not in Foo()
+
+
+def test_message_contains_repeated_composite():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT64, number=1)
+
+ class Baz(proto.Message):
+ foo = proto.RepeatedField(proto.MESSAGE, number=1, message=Foo)
+
+ assert "foo" in Baz(foo=[Foo(bar=42)])
+ assert "foo" in Baz(foo=[Foo()])
+ assert "foo" not in Baz(foo=[])
+ assert "foo" not in Baz()
+
+
+def test_message_eq_primitives():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+ baz = proto.Field(proto.STRING, number=2)
+ bacon = proto.Field(proto.BOOL, number=3)
+
+ assert Foo() == Foo()
+ assert Foo(bar=42, baz="42") == Foo(bar=42, baz="42")
+ assert Foo(bar=42, baz="42") != Foo(baz="42")
+ assert Foo(bar=42, bacon=True) == Foo(bar=42, bacon=True)
+ assert Foo(bar=42, bacon=True) != Foo(bar=42)
+ assert Foo(bar=42, baz="42", bacon=True) != Foo(bar=42, bacon=True)
+ assert Foo(bacon=False) == Foo()
+ assert Foo(bacon=True) != Foo(bacon=False)
+ assert Foo(bar=21 * 2) == Foo(bar=42)
+ assert Foo() == Foo(bar=0)
+ assert Foo() == Foo(bar=0, baz="", bacon=False)
+ assert Foo() != Foo(bar=0, baz="0", bacon=False)
+
+
+def test_message_serialize():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+ baz = proto.Field(proto.STRING, number=2)
+ bacon = proto.Field(proto.BOOL, number=3)
+
+ foo = Foo(bar=42, bacon=True)
+ assert Foo.serialize(foo) == Foo.pb(foo).SerializeToString()
+
+
+def test_message_dict_serialize():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+ baz = proto.Field(proto.STRING, number=2)
+ bacon = proto.Field(proto.BOOL, number=3)
+
+ foo = {"bar": 42, "bacon": True}
+ assert Foo.serialize(foo) == Foo.pb(foo, coerce=True).SerializeToString()
+
+
+def test_message_deserialize():
+ class OldFoo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ class NewFoo(proto.Message):
+ bar = proto.Field(proto.INT64, number=1)
+
+ serialized = OldFoo.serialize(OldFoo(bar=42))
+ new_foo = NewFoo.deserialize(serialized)
+ assert isinstance(new_foo, NewFoo)
+ assert new_foo.bar == 42
+
+
+def test_message_pb():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ assert isinstance(Foo.pb(Foo()), Foo.pb())
+ with pytest.raises(TypeError):
+ Foo.pb(object())
+
+
+def test_invalid_field_access():
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+
+ s = Squid()
+ with pytest.raises(AttributeError):
+ getattr(s, "shell")
+
+
+def test_setattr():
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+
+ s1 = Squid()
+ s2 = Squid(mass_kg=20)
+
+ s1._pb = s2._pb
+
+ assert s1.mass_kg == 20
+
+
+def test_serialize_to_dict():
+ class Squid(proto.Message):
+ # Test primitives, enums, and repeated fields.
+ class Chromatophore(proto.Message):
+ class Color(proto.Enum):
+ UNKNOWN = 0
+ RED = 1
+ BROWN = 2
+ WHITE = 3
+ BLUE = 4
+
+ color = proto.Field(Color, number=1)
+
+ mass_kg = proto.Field(proto.INT32, number=1)
+ chromatophores = proto.RepeatedField(Chromatophore, number=2)
+
+ s = Squid(mass_kg=20)
+ colors = ["RED", "BROWN", "WHITE", "BLUE"]
+ s.chromatophores = [
+ {"color": c} for c in itertools.islice(itertools.cycle(colors), 10)
+ ]
+
+ s_dict = Squid.to_dict(s)
+ assert s_dict["chromatophores"][0]["color"] == 1
+
+ new_s = Squid(s_dict)
+ assert new_s == s
+
+ s_dict = Squid.to_dict(s, use_integers_for_enums=False)
+ assert s_dict["chromatophores"][0]["color"] == "RED"
+
+ s_new_2 = Squid(mass_kg=20)
+ s_dict_2 = Squid.to_dict(s_new_2, including_default_value_fields=False)
+ expected_dict = {"mass_kg": 20}
+ assert s_dict_2 == expected_dict
+
+ s_dict_2 = Squid.to_dict(s_new_2, always_print_fields_with_no_presence=False)
+ expected_dict = {"mass_kg": 20}
+ assert s_dict_2 == expected_dict
+
+ s_dict_2 = Squid.to_dict(
+ s_new_2,
+ including_default_value_fields=False,
+ always_print_fields_with_no_presence=False,
+ )
+ expected_dict = {"mass_kg": 20}
+ assert s_dict_2 == expected_dict
+
+ s_dict_2 = Squid.to_dict(
+ s_new_2,
+ including_default_value_fields=True,
+ )
+ expected_dict = {"mass_kg": 20, "chromatophores": []}
+ assert s_dict_2 == expected_dict
+
+ s_dict_2 = Squid.to_dict(
+ s_new_2,
+ always_print_fields_with_no_presence=True,
+ )
+ expected_dict = {"mass_kg": 20, "chromatophores": []}
+ assert s_dict_2 == expected_dict
+
+ s_dict_2 = Squid.to_dict(
+ s_new_2,
+ including_default_value_fields=True,
+ always_print_fields_with_no_presence=True,
+ )
+ expected_dict = {"mass_kg": 20, "chromatophores": []}
+ assert s_dict_2 == expected_dict
+
+ s_dict_2 = Squid.to_dict(s_new_2)
+ expected_dict = {"mass_kg": 20, "chromatophores": []}
+ assert s_dict_2 == expected_dict
+
+ with pytest.raises(
+ ValueError,
+ match="Arguments.*always_print_fields_with_no_presence.*including_default_value_fields.*must match",
+ ):
+ s_dict_2 = Squid.to_dict(
+ s_new_2,
+ including_default_value_fields=True,
+ always_print_fields_with_no_presence=False,
+ )
+
+ with pytest.raises(
+ ValueError,
+ match="Arguments.*always_print_fields_with_no_presence.*including_default_value_fields.*must match",
+ ):
+ s_dict_2 = Squid.to_dict(
+ s_new_2,
+ including_default_value_fields=False,
+ always_print_fields_with_no_presence=True,
+ )
+
+
+# TODO: https://github.com/googleapis/proto-plus-python/issues/390
+def test_serialize_to_dict_float_precision():
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.FLOAT, number=1)
+
+ s = Squid(mass_kg=3.14159265)
+
+ s_dict = Squid.to_dict(s, float_precision=3)
+ assert s_dict["mass_kg"] == 3.14
+
+
+def test_unknown_field_deserialize():
+ # This is a somewhat common setup: a client uses an older proto definition,
+ # while the server sends the newer definition. The client still needs to be
+ # able to interact with the protos it receives from the server.
+
+ class Octopus_Old(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+
+ class Octopus_New(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+ length_cm = proto.Field(proto.INT32, number=2)
+
+ o_new = Octopus_New(mass_kg=20, length_cm=100)
+ o_ser = Octopus_New.serialize(o_new)
+
+ o_old = Octopus_Old.deserialize(o_ser)
+ assert not hasattr(o_old, "length_cm")
+
+
+def test_unknown_field_deserialize_keep_fields():
+ # This is a somewhat common setup: a client uses an older proto definition,
+ # while the server sends the newer definition. The client still needs to be
+ # able to interact with the protos it receives from the server.
+
+ class Octopus_Old(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+
+ class Octopus_New(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+ length_cm = proto.Field(proto.INT32, number=2)
+
+ o_new = Octopus_New(mass_kg=20, length_cm=100)
+ o_ser = Octopus_New.serialize(o_new)
+
+ o_old = Octopus_Old.deserialize(o_ser)
+ assert not hasattr(o_old, "length_cm")
+
+ o_new = Octopus_New.deserialize(Octopus_Old.serialize(o_old))
+ assert o_new.length_cm == 100
+
+
+def test_unknown_field_from_dict():
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+
+ # By default we don't permit unknown fields
+ with pytest.raises(ValueError):
+ s = Squid({"mass_kg": 20, "length_cm": 100})
+
+ s = Squid({"mass_kg": 20, "length_cm": 100}, ignore_unknown_fields=True)
+ assert not hasattr(s, "length_cm")
+
+
+def test_copy_from():
+ class Mollusc(proto.Message):
+ class Squid(proto.Message):
+ mass_kg = proto.Field(proto.INT32, number=1)
+
+ squid = proto.Field(Squid, number=1)
+
+ m = Mollusc()
+ s = Mollusc.Squid(mass_kg=20)
+ Mollusc.Squid.copy_from(m.squid, s)
+ assert m.squid is not s
+ assert m.squid == s
+
+ s.mass_kg = 30
+ Mollusc.Squid.copy_from(m.squid, Mollusc.Squid.pb(s))
+ assert m.squid == s
+
+ Mollusc.Squid.copy_from(m.squid, {"mass_kg": 10})
+ assert m.squid.mass_kg == 10
+
+ with pytest.raises(TypeError):
+ Mollusc.Squid.copy_from(m.squid, (("mass_kg", 20)))
+
+
+def test_dir():
+ class Mollusc(proto.Message):
+ class Class(proto.Enum):
+ UNKNOWN = 0
+ GASTROPOD = 1
+ BIVALVE = 2
+ CEPHALOPOD = 3
+
+ class Arm(proto.Message):
+ length_cm = proto.Field(proto.INT32, number=1)
+
+ mass_kg = proto.Field(proto.INT32, number=1)
+ class_ = proto.Field(Class, number=2)
+ arms = proto.RepeatedField(Arm, number=3)
+
+ expected = (
+ {
+ # Fields and nested message and enum types
+ "arms",
+ "class_",
+ "mass_kg",
+ "Arm",
+ "Class",
+ }
+ | {
+ # Other methods and attributes
+ "__bool__",
+ "__contains__",
+ "__dict__",
+ "__getattr__",
+ "__getstate__",
+ "__module__",
+ "__setstate__",
+ "__weakref__",
+ }
+ | set(dir(object))
+ ) # Gets the long tail of dunder methods and attributes.
+
+ actual = set(dir(Mollusc()))
+
+ # Check instance names
+ assert actual == expected
+
+ # Check type names
+ expected = (
+ set(dir(type))
+ | {
+ # Class methods from the MessageMeta metaclass
+ "copy_from",
+ "deserialize",
+ "from_json",
+ "meta",
+ "pb",
+ "serialize",
+ "to_dict",
+ "to_json",
+ "wrap",
+ }
+ | {
+ # Nested message and enum types
+ "Arm",
+ "Class",
+ }
+ )
+
+ actual = set(dir(Mollusc))
+ assert actual == expected
+
+
+def test_dir_message_base():
+ assert set(dir(proto.Message)) == set(dir(type))
diff --git a/packages/proto-plus/tests/test_message_filename.py b/packages/proto-plus/tests/test_message_filename.py
new file mode 100644
index 000000000000..dcb2ebfa0bc9
--- /dev/null
+++ b/packages/proto-plus/tests/test_message_filename.py
@@ -0,0 +1,26 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import proto
+
+
+def test_filename_includes_classname_salt():
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ assert (
+ Foo.pb(Foo()).DESCRIPTOR.file.name
+ == "test_message_filename__default_package.foo.proto"
+ )
diff --git a/packages/proto-plus/tests/test_message_filename_with_and_without_manifest.py b/packages/proto-plus/tests/test_message_filename_with_and_without_manifest.py
new file mode 100644
index 000000000000..e67e8472b830
--- /dev/null
+++ b/packages/proto-plus/tests/test_message_filename_with_and_without_manifest.py
@@ -0,0 +1,53 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+
+
+PACKAGE = "a.test.package.with.and.without.manifest"
+__protobuf__ = proto.module(
+ package=PACKAGE,
+ manifest={"This", "That"},
+)
+
+
+class This(proto.Message):
+ this = proto.Field(proto.INT32, number=1)
+
+
+class That(proto.Message):
+ that = proto.Field(proto.INT32, number=1)
+
+
+class NotInManifest(proto.Message):
+ them = proto.Field(proto.INT32, number=1)
+
+
+def test_manifest_causes_exclusion_of_classname_salt():
+
+ assert (
+ This.pb(This()).DESCRIPTOR.file.name
+ == "test_message_filename_with_and_without_manifest.proto"
+ )
+ assert (
+ That.pb(That()).DESCRIPTOR.file.name
+ == "test_message_filename_with_and_without_manifest.proto"
+ )
+
+ assert (
+ NotInManifest.pb(NotInManifest()).DESCRIPTOR.file.name
+ == "test_message_filename_with_and_without_manifest_"
+ + PACKAGE
+ + ".notinmanifest.proto"
+ )
diff --git a/packages/proto-plus/tests/test_message_filename_with_manifest.py b/packages/proto-plus/tests/test_message_filename_with_manifest.py
new file mode 100644
index 000000000000..aa96028ef792
--- /dev/null
+++ b/packages/proto-plus/tests/test_message_filename_with_manifest.py
@@ -0,0 +1,40 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+
+PACKAGE = "a.test.package.with.manifest"
+__protobuf__ = proto.module(
+ package=PACKAGE,
+ manifest={"ThisFoo", "ThisBar"},
+)
+
+
+class ThisFoo(proto.Message):
+ foo = proto.Field(proto.INT32, number=1)
+
+
+class ThisBar(proto.Message):
+ bar = proto.Field(proto.INT32, number=2)
+
+
+def test_manifest_causes_exclusion_of_classname_salt():
+ assert (
+ ThisFoo.pb(ThisFoo()).DESCRIPTOR.file.name
+ == "test_message_filename_with_manifest.proto"
+ )
+ assert (
+ ThisBar.pb(ThisBar()).DESCRIPTOR.file.name
+ == "test_message_filename_with_manifest.proto"
+ )
diff --git a/packages/proto-plus/tests/test_message_nested.py b/packages/proto-plus/tests/test_message_nested.py
new file mode 100644
index 000000000000..41af9507f22e
--- /dev/null
+++ b/packages/proto-plus/tests/test_message_nested.py
@@ -0,0 +1,64 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import proto
+
+
+def test_singly_nested_message():
+ class Foo(proto.Message):
+ class Bar(proto.Message):
+ value = proto.Field(proto.INT32, number=1)
+
+ bar = proto.Field(proto.MESSAGE, number=1, message=Bar)
+
+ foo = Foo(bar=Foo.Bar(value=42))
+ assert foo.bar.value == 42
+
+
+def test_multiply_nested_message():
+ class Foo(proto.Message):
+ class Bar(proto.Message):
+ class Baz(proto.Message):
+ value = proto.Field(proto.INT32, number=1)
+
+ baz = proto.Field(proto.MESSAGE, number=1, message=Baz)
+
+ bar = proto.Field(proto.MESSAGE, number=1, message=Bar)
+
+ foo = Foo(bar=Foo.Bar(baz=Foo.Bar.Baz(value=42)))
+ assert foo.bar.baz.value == 42
+
+
+def test_forking_nested_messages():
+ class Foo(proto.Message):
+ class Bar(proto.Message):
+ spam = proto.Field(proto.STRING, number=1)
+ eggs = proto.Field(proto.BOOL, number=2)
+
+ class Baz(proto.Message):
+ class Bacon(proto.Message):
+ value = proto.Field(proto.INT32, number=1)
+
+ bacon = proto.Field(proto.MESSAGE, number=1, message=Bacon)
+
+ bar = proto.Field(proto.MESSAGE, number=1, message=Bar)
+ baz = proto.Field(proto.MESSAGE, number=2, message=Baz)
+
+ foo = Foo(
+ bar={"spam": "xyz", "eggs": False},
+ baz=Foo.Baz(bacon=Foo.Baz.Bacon(value=42)),
+ )
+ assert foo.bar.spam == "xyz"
+ assert not foo.bar.eggs
+ assert foo.baz.bacon.value == 42
diff --git a/packages/proto-plus/tests/test_message_pickling.py b/packages/proto-plus/tests/test_message_pickling.py
new file mode 100644
index 000000000000..dd97403c1a79
--- /dev/null
+++ b/packages/proto-plus/tests/test_message_pickling.py
@@ -0,0 +1,51 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import itertools
+import pickle
+
+import pytest
+
+import proto
+
+
+class Squid(proto.Message):
+ # Test primitives, enums, and repeated fields.
+ class Chromatophore(proto.Message):
+ class Color(proto.Enum):
+ UNKNOWN = 0
+ RED = 1
+ BROWN = 2
+ WHITE = 3
+ BLUE = 4
+
+ color = proto.Field(Color, number=1)
+
+ mass_kg = proto.Field(proto.INT32, number=1)
+ chromatophores = proto.RepeatedField(Chromatophore, number=2)
+
+
+def test_pickling():
+
+ s = Squid(mass_kg=20)
+ colors = ["RED", "BROWN", "WHITE", "BLUE"]
+ s.chromatophores = [
+ {"color": c} for c in itertools.islice(itertools.cycle(colors), 10)
+ ]
+
+ pickled = pickle.dumps(s)
+
+ unpickled = pickle.loads(pickled)
+
+ assert unpickled == s
diff --git a/packages/proto-plus/tests/test_modules.py b/packages/proto-plus/tests/test_modules.py
new file mode 100644
index 000000000000..7ab0e88741af
--- /dev/null
+++ b/packages/proto-plus/tests/test_modules.py
@@ -0,0 +1,135 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from unittest import mock
+import inspect
+import sys
+
+from google.protobuf import wrappers_pb2
+
+import proto
+
+
+def test_module_package():
+ sys.modules[__name__].__protobuf__ = proto.module(package="spam.eggs.v1")
+ try:
+
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ marshal = proto.Marshal(name="spam.eggs.v1")
+
+ assert Foo.meta.package == "spam.eggs.v1"
+ assert Foo.pb() in marshal._rules
+ finally:
+ del sys.modules[__name__].__protobuf__
+
+
+def test_module_package_cross_api():
+ sys.modules[__name__].__protobuf__ = proto.module(package="spam.eggs.v1")
+ try:
+
+ class Baz(proto.Message):
+ foo = proto.RepeatedField(proto.INT64, number=1)
+
+ marshal = proto.Marshal(name="spam.eggs.v1")
+
+ assert Baz.meta.package == "spam.eggs.v1"
+ assert Baz.pb() in marshal._rules
+
+ sys.modules[__name__].__protobuf__ = proto.module(package="ham.pancakes.v1")
+
+ class AnotherMessage(proto.Message):
+ qux = proto.Field(proto.MESSAGE, number=1, message=Baz)
+
+ marshal = proto.Marshal(name="ham.pancakes.v1")
+
+ assert AnotherMessage.meta.package == "ham.pancakes.v1"
+ assert AnotherMessage.pb() in marshal._rules
+ # Confirm that Baz.pb() is no longer present in marshal._rules
+ assert Baz.pb() not in marshal._rules
+
+ # Test using multiple packages together
+ # See https://github.com/googleapis/proto-plus-python/issues/349.
+ msg = AnotherMessage(qux=Baz())
+ assert type(msg) == AnotherMessage
+ finally:
+ del sys.modules[__name__].__protobuf__
+
+
+def test_module_package_explicit_marshal():
+ sys.modules[__name__].__protobuf__ = proto.module(
+ package="spam.eggs.v1",
+ marshal="foo",
+ )
+ try:
+
+ class Foo(proto.Message):
+ bar = proto.Field(proto.INT32, number=1)
+
+ marshal = proto.Marshal(name="foo")
+
+ assert Foo.meta.package == "spam.eggs.v1"
+ assert Foo.pb() in marshal._rules
+ finally:
+ del sys.modules[__name__].__protobuf__
+
+
+def test_module_manifest():
+ __protobuf__ = proto.module(
+ manifest={"Foo", "Bar", "Baz"},
+ package="spam.eggs.v1",
+ )
+
+ # We want to fake a module, but modules have attribute access, and
+ # `frame.f_locals` is a dictionary. Since we only actually care about
+ # getattr, this is reasonably easy to shim over.
+ frame = inspect.currentframe()
+ with mock.patch.object(inspect, "getmodule") as getmodule:
+ getmodule.side_effect = lambda *a: View(frame.f_locals)
+
+ class Foo(proto.Message):
+ a = proto.Field(wrappers_pb2.Int32Value, number=1)
+
+ class Bar(proto.Message):
+ b = proto.Field(proto.MESSAGE, number=1, message=Foo)
+
+ assert not Foo.pb()
+ assert not Bar.pb()
+
+ class Baz(proto.Message):
+ c = proto.Field(wrappers_pb2.BoolValue, number=1)
+
+ assert Foo.pb()
+ assert Bar.pb()
+ assert Baz.pb()
+
+ foo = Foo(a=12)
+ bar = Bar(b=Foo(a=24))
+ baz = Baz(c=False)
+ assert foo.a == 12
+ assert bar.b.a == 24
+ assert baz.c is False
+
+
+class View:
+ """A view around a mapping, for attribute-like access."""
+
+ def __init__(self, mapping):
+ self._mapping = mapping
+
+ def __getattr__(self, name):
+ if name not in self._mapping:
+ raise AttributeError
+ return self._mapping[name]
diff --git a/packages/proto-plus/tests/zone.py b/packages/proto-plus/tests/zone.py
new file mode 100644
index 000000000000..90bea6a878c0
--- /dev/null
+++ b/packages/proto-plus/tests/zone.py
@@ -0,0 +1,31 @@
+# Copyright (C) 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import proto
+
+
+__protobuf__ = proto.module(
+ package="ocean.zone.v1",
+ manifest={
+ "Zone",
+ },
+)
+
+
+class Zone(proto.Enum):
+ EPIPELAGIC = 0
+ MESOPELAGIC = 1
+ BATHYPELAGIC = 2
+ ABYSSOPELAGIC = 3