Compare commits
	
		
			62 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | ee362a7a73 | ||
|  | 261e55b2c8 | ||
|  | 98930ce0d7 | ||
|  | d7d277eb0d | ||
|  | 3860c0ab11 | ||
|  | cd1c2dc3b5 | ||
|  | be2a24d15c | ||
|  | a5effb219a | ||
|  | 2d57f0d122 | ||
|  | a68505b80e | ||
|  | 2f9497e064 | ||
|  | 33964b883e | ||
|  | ec7574086d | ||
|  | 8a42027bc9 | ||
|  | 71737cf696 | ||
|  | 659ddd9c44 | ||
|  | 5b6997870a | ||
|  | cdf7645722 | ||
|  | ca20069ca3 | ||
|  | 59a4a7da43 | ||
|  | 15af4367e5 | ||
|  | ec5683e572 | ||
|  | 20150fdcf3 | ||
|  | d11b7d04c5 | ||
|  | e2d35f4696 | ||
|  | c3f08b9ef2 | ||
|  | 24d44898f4 | ||
|  | 074448c996 | ||
|  | 0fe557bd3c | ||
|  | 1a87ea43a1 | ||
|  | 983e0895a2 | ||
|  | 4a2baf3f0a | ||
|  | 8f0caf1db2 | ||
|  | c50d9e2fdc | ||
|  | 35548cb43e | ||
|  | b711d1e11f | ||
|  | 917de09bb6 | ||
|  | 1f7f39049e | ||
|  | 3d001a2a1a | ||
|  | de61ddab21 | ||
|  | 5e2d9febea | ||
|  | f6af077ffe | ||
|  | 92088ebda8 | ||
|  | c3e3837f71 | ||
|  | 6bd9c7835c | ||
|  | 6ec902c1b5 | ||
|  | 960dba2ae8 | ||
|  | 4b4bdefb6f | ||
|  | dfa0a56b39 | ||
|  | dd4873dfba | ||
|  | 91f586f7d7 | ||
|  | 33fb83faad | ||
|  | 77c04414f5 | ||
|  | 6969ff7ff6 | ||
|  | 13e08fdaa8 | ||
|  | 6775632f77 | ||
|  | b12f1e4e61 | ||
|  | 7e9ba0866c | ||
|  | 3546f55146 | ||
|  | 499489f1d3 | ||
|  | 5759e323bd | ||
|  | c762c9c549 | 
							
								
								
									
										18
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -4,6 +4,24 @@ on: [push, pull_request] | ||||
|  | ||||
| jobs: | ||||
|  | ||||
|   check-formatting: | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     name: Consult black on python formatting | ||||
|  | ||||
|     steps: | ||||
|       - uses: actions/checkout@v1 | ||||
|       - uses: actions/setup-python@v1 | ||||
|         with: | ||||
|           python-version: 3.7 | ||||
|       - uses: dschep/install-pipenv-action@v1 | ||||
|       - name: Install dependencies | ||||
|         run: | | ||||
|           pipenv install --dev --python ${pythonLocation}/python | ||||
|       - name: Run black | ||||
|         run: | | ||||
|           pipenv run black . --check --diff --exclude tests/output_ | ||||
|  | ||||
|   run-tests: | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|   | ||||
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,12 +4,9 @@ | ||||
| .pytest_cache | ||||
| .python-version | ||||
| build/ | ||||
| betterproto/tests/*.bin | ||||
| betterproto/tests/*_pb2.py | ||||
| betterproto/tests/*.py | ||||
| !betterproto/tests/generate.py | ||||
| !betterproto/tests/test_*.py | ||||
| betterproto/tests/output_* | ||||
| **/__pycache__ | ||||
| dist | ||||
| **/*.egg-info | ||||
| output | ||||
| .idea | ||||
							
								
								
									
										17
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. | ||||
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||||
|  | ||||
| ## [1.2.5] - 2020-04-27 | ||||
|  | ||||
| - Add .j2 suffix to python template names to avoid confusing certain build tools [#72](https://github.com/danielgtaylor/python-betterproto/pull/72) | ||||
|  | ||||
| ## [1.2.4] - 2020-04-26 | ||||
|  | ||||
| - Enforce utf-8 for reading the readme in setup.py [#67](https://github.com/danielgtaylor/python-betterproto/pull/67) | ||||
| - Only import types from grpclib when type checking [#52](https://github.com/danielgtaylor/python-betterproto/pull/52) | ||||
| - Improve performance of serialize/deserialize by caching type information of fields in class [#46](https://github.com/danielgtaylor/python-betterproto/pull/46) | ||||
| - Support using Google's wrapper types as RPC output values [#40](https://github.com/danielgtaylor/python-betterproto/pull/40) | ||||
| - Fixes issue where protoc did not recognize plugin.py as win32 application [#38](https://github.com/danielgtaylor/python-betterproto/pull/38) | ||||
| - Fix services using non-pythonified field names [#34](https://github.com/danielgtaylor/python-betterproto/pull/34) | ||||
| - Add ability to provide metadata, timeout & deadline args to requests [#32](https://github.com/danielgtaylor/python-betterproto/pull/32) | ||||
|  | ||||
| ## [1.2.3] - 2020-04-15 | ||||
|  | ||||
| - Exclude empty lists from `to_dict` by default [#16](https://github.com/danielgtaylor/python-betterproto/pull/16) | ||||
| @@ -44,7 +58,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | ||||
|  | ||||
| - Initial release | ||||
|  | ||||
| [unreleased]: https://github.com/danielgtaylor/python-betterproto/compare/v1.2.3...HEAD | ||||
| [1.2.5]: https://github.com/danielgtaylor/python-betterproto/compare/v1.2.4...v1.2.5 | ||||
| [1.2.4]: https://github.com/danielgtaylor/python-betterproto/compare/v1.2.3...v1.2.4 | ||||
| [1.2.3]: https://github.com/danielgtaylor/python-betterproto/compare/v1.2.2...v1.2.3 | ||||
| [1.2.2]: https://github.com/danielgtaylor/python-betterproto/compare/v1.2.1...v1.2.2 | ||||
| [1.2.1]: https://github.com/danielgtaylor/python-betterproto/compare/v1.2.0...v1.2.1 | ||||
|   | ||||
							
								
								
									
										2
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Pipfile
									
									
									
									
									
								
							| @@ -8,8 +8,8 @@ flake8 = "*" | ||||
| mypy = "*" | ||||
| isort = "*" | ||||
| pytest = "*" | ||||
| pytest-asyncio = "*" | ||||
| rope = "*" | ||||
| v = {editable = true,version = "*"} | ||||
|  | ||||
| [packages] | ||||
| protobuf = "*" | ||||
|   | ||||
							
								
								
									
										444
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										444
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,444 @@ | ||||
| { | ||||
|     "_meta": { | ||||
|         "hash": { | ||||
|             "sha256": "44ae793965dc2b6ec17f0435a388846248b8a703cf857470b66af84227535950" | ||||
|         }, | ||||
|         "pipfile-spec": 6, | ||||
|         "requires": { | ||||
|             "python_version": "3.6" | ||||
|         }, | ||||
|         "sources": [ | ||||
|             { | ||||
|                 "name": "pypi", | ||||
|                 "url": "https://pypi.org/simple", | ||||
|                 "verify_ssl": true | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     "default": { | ||||
|         "appdirs": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", | ||||
|                 "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" | ||||
|             ], | ||||
|             "version": "==1.4.4" | ||||
|         }, | ||||
|         "attrs": { | ||||
|             "hashes": [ | ||||
|                 "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", | ||||
|                 "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" | ||||
|             ], | ||||
|             "version": "==19.3.0" | ||||
|         }, | ||||
|         "backports-datetime-fromisoformat": { | ||||
|             "hashes": [ | ||||
|                 "sha256:9577a2a9486cd7383a5f58b23bb8e81cf0821dbbc0eb7c87d3fa198c1df40f5c" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.0.0" | ||||
|         }, | ||||
|         "black": { | ||||
|             "hashes": [ | ||||
|                 "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", | ||||
|                 "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==19.10b0" | ||||
|         }, | ||||
|         "click": { | ||||
|             "hashes": [ | ||||
|                 "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", | ||||
|                 "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" | ||||
|             ], | ||||
|             "version": "==7.1.2" | ||||
|         }, | ||||
|         "dataclasses": { | ||||
|             "hashes": [ | ||||
|                 "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f", | ||||
|                 "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.6" | ||||
|         }, | ||||
|         "grpclib": { | ||||
|             "hashes": [ | ||||
|                 "sha256:b27d56c987b89023d5640fe9668943e49b46703fc85d8182a58c9f3b19120cdc" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.3.2rc1" | ||||
|         }, | ||||
|         "h2": { | ||||
|             "hashes": [ | ||||
|                 "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5", | ||||
|                 "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14" | ||||
|             ], | ||||
|             "version": "==3.2.0" | ||||
|         }, | ||||
|         "hpack": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89", | ||||
|                 "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2" | ||||
|             ], | ||||
|             "version": "==3.0.0" | ||||
|         }, | ||||
|         "hyperframe": { | ||||
|             "hashes": [ | ||||
|                 "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40", | ||||
|                 "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f" | ||||
|             ], | ||||
|             "version": "==5.2.0" | ||||
|         }, | ||||
|         "jinja2": { | ||||
|             "hashes": [ | ||||
|                 "sha256:c10142f819c2d22bdcd17548c46fa9b77cf4fda45097854c689666bf425e7484", | ||||
|                 "sha256:c922560ac46888d47384de1dbdc3daaa2ea993af4b26a436dec31fa2c19ec668" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.0.0a1" | ||||
|         }, | ||||
|         "markupsafe": { | ||||
|             "hashes": [ | ||||
|                 "sha256:06358015a4dee8ee23ae426bf885616ab3963622defd829eb45b44e3dee3515f", | ||||
|                 "sha256:0b0c4fc852c5f02c6277ef3b33d23fcbe89b1b227460423e3335374da046b6db", | ||||
|                 "sha256:267677fc42afed5094fc5ea1c4236bbe4b6a00fe4b08e93451e65ae9048139c7", | ||||
|                 "sha256:303cb70893e2c345588fb5d5b86e0ca369f9bb56942f03064c5e3e75fa7a238a", | ||||
|                 "sha256:3c9b624a0d9ed5a5093ac4edc4e823e6b125441e60ef35d36e6f4a6fdacd5054", | ||||
|                 "sha256:42033e14cae1f6c86fc0c3e90d04d08ce73ac8e46ba420a0d22d545c2abd4977", | ||||
|                 "sha256:4e4a99b6af7bdc0856b50020c095848ec050356a001e1f751510aef6ab14d0e0", | ||||
|                 "sha256:4eb07faad54bb07427d848f31030a65a49ebb0cec0b30674f91cf1ddd456bfe4", | ||||
|                 "sha256:63a7161cd8c2bc563feeda45df62f42c860dd0675e2b8da2667f25bb3c95eaba", | ||||
|                 "sha256:68e0fd039b68d2945b4beb947d4023ca7f8e95b708031c345762efba214ea761", | ||||
|                 "sha256:8092a63397025c2f655acd42784b2a1528339b90b987beb9253f22e8cdbb36c3", | ||||
|                 "sha256:841218860683c0f2223e24756843d84cc49cccdae6765e04962607754a52d3e0", | ||||
|                 "sha256:94076b2314bd2f6cfae508ad65b4d493e3a58a50112b7a2cbb6287bdbc404ae8", | ||||
|                 "sha256:9d22aff1c5322e402adfb3ce40839a5056c353e711c033798cf4f02eb9f5124d", | ||||
|                 "sha256:b0e4584f62b3e5f5c1a7bcefd2b52f236505e6ef032cc508caa4f4c8dc8d3af1", | ||||
|                 "sha256:b1163ffc1384d242964426a8164da12dbcdbc0de18ea36e2c34b898ed38c3b45", | ||||
|                 "sha256:beac28ed60c8e838301226a7a85841d0af2068eba2dcb1a58c2d32d6c05e440e", | ||||
|                 "sha256:c29f096ce79c03054a1101d6e5fe6bf04b0bb489165d5e0e9653fb4fe8048ee1", | ||||
|                 "sha256:c58779966d53e5f14ba393d64e2402a7926601d1ac8adeb4e83893def79d0428", | ||||
|                 "sha256:cfe14b37908eaf7d5506302987228bff69e1b8e7071ccd4e70fd0283b1b47f0b", | ||||
|                 "sha256:e834249c45aa9837d0753351cdca61a4b8b383cc9ad0ff2325c97ff7b69e72a6", | ||||
|                 "sha256:eed1b234c4499811ee85bcefa22ef5e466e75d132502226ed29740d593316c1f" | ||||
|             ], | ||||
|             "version": "==2.0.0a1" | ||||
|         }, | ||||
|         "multidict": { | ||||
|             "hashes": [ | ||||
|                 "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", | ||||
|                 "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", | ||||
|                 "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", | ||||
|                 "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", | ||||
|                 "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", | ||||
|                 "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", | ||||
|                 "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", | ||||
|                 "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", | ||||
|                 "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", | ||||
|                 "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", | ||||
|                 "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", | ||||
|                 "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", | ||||
|                 "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", | ||||
|                 "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", | ||||
|                 "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", | ||||
|                 "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", | ||||
|                 "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" | ||||
|             ], | ||||
|             "version": "==4.7.6" | ||||
|         }, | ||||
|         "pathspec": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", | ||||
|                 "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" | ||||
|             ], | ||||
|             "version": "==0.8.0" | ||||
|         }, | ||||
|         "protobuf": { | ||||
|             "hashes": [ | ||||
|                 "sha256:04d0b2bd99050d09393875a5a25fd12337b17f3ac2e29c0c1b8e65b277cbfe72", | ||||
|                 "sha256:05288e44638e91498f13127a3699a6528dec6f9d3084d60959d721bfb9ea5b98", | ||||
|                 "sha256:175d85370947f89e33b3da93f4ccdda3f326bebe3e599df5915ceb7f804cd9df", | ||||
|                 "sha256:440a8c77531b3652f24999b249256ed01fd44c498ab0973843066681bd276685", | ||||
|                 "sha256:49fb6fab19cd3f30fa0e976eeedcbf2558e9061e5fa65b4fe51ded1f4002e04d", | ||||
|                 "sha256:4c7cae1f56056a4a2a2e3b00b26ab8550eae738bd9548f4ea0c2fcb88ed76ae5", | ||||
|                 "sha256:519abfacbb421c3591d26e8daf7a4957763428db7267f7207e3693e29f6978db", | ||||
|                 "sha256:60f32af25620abc4d7928d8197f9f25d49d558c5959aa1e08c686f974ac0b71a", | ||||
|                 "sha256:613ac49f6db266fba243daf60fb32af107cfe3678e5c003bb40a381b6786389d", | ||||
|                 "sha256:954bb14816edd24e746ba1a6b2d48c43576393bbde2fb8e1e3bd6d4504c7feac", | ||||
|                 "sha256:9b1462c033a2cee7f4e8eb396905c69de2c532c3b835ff8f71f8e5fb77c38023", | ||||
|                 "sha256:c0767f4d93ce4288475afe0571663c78870924f1f8881efd5406c10f070c75e4", | ||||
|                 "sha256:c45f5980ce32879391144b5766120fd7b8803129f127ce36bd060dd38824801f", | ||||
|                 "sha256:eeb7502f59e889a88bcb59f299493e215d1864f3d75335ea04a413004eb4fe24", | ||||
|                 "sha256:fdb1742f883ee4662e39fcc5916f2725fec36a5191a52123fec60f8c53b70495", | ||||
|                 "sha256:fe554066c4962c2db0a1d4752655223eb948d2bfa0fb1c4a7f2c00ec07324f1c" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.12.1" | ||||
|         }, | ||||
|         "regex": { | ||||
|             "hashes": [ | ||||
|                 "sha256:1386e75c9d1574f6aa2e4eb5355374c8e55f9aac97e224a8a5a6abded0f9c927", | ||||
|                 "sha256:27ff7325b297fb6e5ebb70d10437592433601c423f5acf86e5bc1ee2919b9561", | ||||
|                 "sha256:329ba35d711e3428db6b45a53b1b13a0a8ba07cbbcf10bbed291a7da45f106c3", | ||||
|                 "sha256:3a9394197664e35566242686d84dfd264c07b20f93514e2e09d3c2b3ffdf78fe", | ||||
|                 "sha256:51f17abbe973c7673a61863516bdc9c0ef467407a940f39501e786a07406699c", | ||||
|                 "sha256:579ea215c81d18da550b62ff97ee187b99f1b135fd894a13451e00986a080cad", | ||||
|                 "sha256:70c14743320a68c5dac7fc5a0f685be63bc2024b062fe2aaccc4acc3d01b14a1", | ||||
|                 "sha256:7e61be8a2900897803c293247ef87366d5df86bf701083b6c43119c7c6c99108", | ||||
|                 "sha256:8044d1c085d49673aadb3d7dc20ef5cb5b030c7a4fa253a593dda2eab3059929", | ||||
|                 "sha256:89d76ce33d3266173f5be80bd4efcbd5196cafc34100fdab814f9b228dee0fa4", | ||||
|                 "sha256:99568f00f7bf820c620f01721485cad230f3fb28f57d8fbf4a7967ec2e446994", | ||||
|                 "sha256:a7c37f048ec3920783abab99f8f4036561a174f1314302ccfa4e9ad31cb00eb4", | ||||
|                 "sha256:c2062c7d470751b648f1cacc3f54460aebfc261285f14bc6da49c6943bd48bdd", | ||||
|                 "sha256:c9bce6e006fbe771a02bda468ec40ffccbf954803b470a0345ad39c603402577", | ||||
|                 "sha256:ce367d21f33e23a84fb83a641b3834dd7dd8e9318ad8ff677fbfae5915a239f7", | ||||
|                 "sha256:ce450ffbfec93821ab1fea94779a8440e10cf63819be6e176eb1973a6017aff5", | ||||
|                 "sha256:ce5cc53aa9fbbf6712e92c7cf268274eaff30f6bd12a0754e8133d85a8fb0f5f", | ||||
|                 "sha256:d466967ac8e45244b9dfe302bbe5e3337f8dc4dec8d7d10f5e950d83b140d33a", | ||||
|                 "sha256:d881c2e657c51d89f02ae4c21d9adbef76b8325fe4d5cf0e9ad62f850f3a98fd", | ||||
|                 "sha256:e565569fc28e3ba3e475ec344d87ed3cd8ba2d575335359749298a0899fe122e", | ||||
|                 "sha256:ea55b80eb0d1c3f1d8d784264a6764f931e172480a2f1868f2536444c5f01e01" | ||||
|             ], | ||||
|             "version": "==2020.5.14" | ||||
|         }, | ||||
|         "six": { | ||||
|             "hashes": [ | ||||
|                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", | ||||
|                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" | ||||
|             ], | ||||
|             "version": "==1.15.0" | ||||
|         }, | ||||
|         "stringcase": { | ||||
|             "hashes": [ | ||||
|                 "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.2.0" | ||||
|         }, | ||||
|         "toml": { | ||||
|             "hashes": [ | ||||
|                 "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", | ||||
|                 "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" | ||||
|             ], | ||||
|             "version": "==0.10.1" | ||||
|         }, | ||||
|         "typed-ast": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", | ||||
|                 "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", | ||||
|                 "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", | ||||
|                 "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", | ||||
|                 "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", | ||||
|                 "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", | ||||
|                 "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", | ||||
|                 "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", | ||||
|                 "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", | ||||
|                 "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", | ||||
|                 "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", | ||||
|                 "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", | ||||
|                 "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", | ||||
|                 "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", | ||||
|                 "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", | ||||
|                 "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", | ||||
|                 "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", | ||||
|                 "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", | ||||
|                 "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", | ||||
|                 "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", | ||||
|                 "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" | ||||
|             ], | ||||
|             "version": "==1.4.1" | ||||
|         } | ||||
|     }, | ||||
|     "develop": { | ||||
|         "attrs": { | ||||
|             "hashes": [ | ||||
|                 "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", | ||||
|                 "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" | ||||
|             ], | ||||
|             "version": "==19.3.0" | ||||
|         }, | ||||
|         "flake8": { | ||||
|             "hashes": [ | ||||
|                 "sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634", | ||||
|                 "sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.8.2" | ||||
|         }, | ||||
|         "importlib-metadata": { | ||||
|             "hashes": [ | ||||
|                 "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", | ||||
|                 "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" | ||||
|             ], | ||||
|             "markers": "python_version < '3.8'", | ||||
|             "version": "==1.6.0" | ||||
|         }, | ||||
|         "isort": { | ||||
|             "hashes": [ | ||||
|                 "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", | ||||
|                 "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==4.3.21" | ||||
|         }, | ||||
|         "mccabe": { | ||||
|             "hashes": [ | ||||
|                 "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", | ||||
|                 "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" | ||||
|             ], | ||||
|             "version": "==0.6.1" | ||||
|         }, | ||||
|         "more-itertools": { | ||||
|             "hashes": [ | ||||
|                 "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be", | ||||
|                 "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982" | ||||
|             ], | ||||
|             "version": "==8.3.0" | ||||
|         }, | ||||
|         "mypy": { | ||||
|             "hashes": [ | ||||
|                 "sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2", | ||||
|                 "sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1", | ||||
|                 "sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164", | ||||
|                 "sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761", | ||||
|                 "sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce", | ||||
|                 "sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27", | ||||
|                 "sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754", | ||||
|                 "sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae", | ||||
|                 "sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9", | ||||
|                 "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600", | ||||
|                 "sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65", | ||||
|                 "sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8", | ||||
|                 "sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913", | ||||
|                 "sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.770" | ||||
|         }, | ||||
|         "mypy-extensions": { | ||||
|             "hashes": [ | ||||
|                 "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", | ||||
|                 "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" | ||||
|             ], | ||||
|             "version": "==0.4.3" | ||||
|         }, | ||||
|         "packaging": { | ||||
|             "hashes": [ | ||||
|                 "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", | ||||
|                 "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" | ||||
|             ], | ||||
|             "version": "==20.4" | ||||
|         }, | ||||
|         "pluggy": { | ||||
|             "hashes": [ | ||||
|                 "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", | ||||
|                 "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" | ||||
|             ], | ||||
|             "version": "==0.13.1" | ||||
|         }, | ||||
|         "py": { | ||||
|             "hashes": [ | ||||
|                 "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", | ||||
|                 "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" | ||||
|             ], | ||||
|             "version": "==1.8.1" | ||||
|         }, | ||||
|         "pycodestyle": { | ||||
|             "hashes": [ | ||||
|                 "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", | ||||
|                 "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" | ||||
|             ], | ||||
|             "version": "==2.6.0" | ||||
|         }, | ||||
|         "pyflakes": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", | ||||
|                 "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" | ||||
|             ], | ||||
|             "version": "==2.2.0" | ||||
|         }, | ||||
|         "pyparsing": { | ||||
|             "hashes": [ | ||||
|                 "sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492", | ||||
|                 "sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a" | ||||
|             ], | ||||
|             "version": "==3.0.0a1" | ||||
|         }, | ||||
|         "pytest": { | ||||
|             "hashes": [ | ||||
|                 "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3", | ||||
|                 "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==5.4.2" | ||||
|         }, | ||||
|         "pytest-asyncio": { | ||||
|             "hashes": [ | ||||
|                 "sha256:475bd2f3dc0bc11d2463656b3cbaafdbec5a47b47508ea0b329ee693040eebd2" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.12.0" | ||||
|         }, | ||||
|         "rope": { | ||||
|             "hashes": [ | ||||
|                 "sha256:658ad6705f43dcf3d6df379da9486529cf30e02d9ea14c5682aa80eb33b649e1" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.17.0" | ||||
|         }, | ||||
|         "six": { | ||||
|             "hashes": [ | ||||
|                 "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", | ||||
|                 "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" | ||||
|             ], | ||||
|             "version": "==1.15.0" | ||||
|         }, | ||||
|         "typed-ast": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", | ||||
|                 "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", | ||||
|                 "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", | ||||
|                 "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", | ||||
|                 "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", | ||||
|                 "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", | ||||
|                 "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", | ||||
|                 "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", | ||||
|                 "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", | ||||
|                 "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", | ||||
|                 "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", | ||||
|                 "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", | ||||
|                 "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", | ||||
|                 "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", | ||||
|                 "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", | ||||
|                 "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", | ||||
|                 "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", | ||||
|                 "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", | ||||
|                 "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", | ||||
|                 "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", | ||||
|                 "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" | ||||
|             ], | ||||
|             "version": "==1.4.1" | ||||
|         }, | ||||
|         "typing-extensions": { | ||||
|             "hashes": [ | ||||
|                 "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", | ||||
|                 "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", | ||||
|                 "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" | ||||
|             ], | ||||
|             "version": "==3.7.4.2" | ||||
|         }, | ||||
|         "wcwidth": { | ||||
|             "hashes": [ | ||||
|                 "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", | ||||
|                 "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" | ||||
|             ], | ||||
|             "version": "==0.1.9" | ||||
|         }, | ||||
|         "zipp": { | ||||
|             "hashes": [ | ||||
|                 "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", | ||||
|                 "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" | ||||
|             ], | ||||
|             "version": "==3.1.0" | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										39
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								README.md
									
									
									
									
									
								
							| @@ -307,14 +307,42 @@ $ pipenv shell | ||||
| $ pip install -e . | ||||
| ``` | ||||
|  | ||||
| ### Code style | ||||
|  | ||||
| This project enforces [black](https://github.com/psf/black) python code formatting. | ||||
|  | ||||
| Before commiting changes run: | ||||
|  | ||||
| ```bash | ||||
| pipenv run black . | ||||
| ``` | ||||
|  | ||||
| To avoid merge conflicts later, non-black formatted python code will fail in CI. | ||||
|  | ||||
| ### Tests | ||||
|  | ||||
| There are two types of tests: | ||||
|  | ||||
| 1. Manually-written tests for some behavior of the library | ||||
| 2. Proto files and JSON inputs for automated tests | ||||
| 1. Standard tests | ||||
| 2. Custom tests | ||||
|  | ||||
| For #2, you can add a new `*.proto` file into the `betterproto/tests` directory along with a sample `*.json` input and it will get automatically picked up. | ||||
| #### Standard tests | ||||
|  | ||||
| Adding a standard test case is easy. | ||||
|  | ||||
| - Create a new directory `betterproto/tests/inputs/<name>` | ||||
|   - add `<name>.proto`  with a message called `Test` | ||||
|   - add `<name>.json` with some test data | ||||
|  | ||||
| It will be picked up automatically when you run the tests. | ||||
|  | ||||
| - See also: [Standard Tests Development Guide](betterproto/tests/README.md) | ||||
|  | ||||
| #### Custom tests | ||||
|  | ||||
| Custom tests are found in `tests/test_*.py` and are run with pytest. | ||||
|  | ||||
| #### Running | ||||
|  | ||||
| Here's how to run the tests. | ||||
|  | ||||
| @@ -322,7 +350,7 @@ Here's how to run the tests. | ||||
| # Generate assets from sample .proto files | ||||
| $ pipenv run generate | ||||
|  | ||||
| # Run the tests | ||||
| # Run all tests | ||||
| $ pipenv run test | ||||
| ``` | ||||
|  | ||||
| @@ -340,6 +368,9 @@ $ pipenv run test | ||||
| - [x] Refs to nested types | ||||
| - [x] Imports in proto files | ||||
| - [x] Well-known Google types | ||||
|   - [ ] Support as request input | ||||
|   - [ ] Support as response output | ||||
|     - [ ] Automatically wrap/unwrap responses | ||||
| - [x] OneOf support | ||||
|   - [x] Basic support on the wire | ||||
|   - [x] Check which was set from the group | ||||
|   | ||||
| @@ -11,10 +11,12 @@ from typing import ( | ||||
|     Any, | ||||
|     AsyncGenerator, | ||||
|     Callable, | ||||
|     Collection, | ||||
|     Dict, | ||||
|     Generator, | ||||
|     Iterable, | ||||
|     List, | ||||
|     Mapping, | ||||
|     Optional, | ||||
|     SupportsBytes, | ||||
|     Tuple, | ||||
| @@ -25,7 +27,7 @@ from typing import ( | ||||
|     TYPE_CHECKING, | ||||
| ) | ||||
|  | ||||
| import grpclib.client | ||||
|  | ||||
| import grpclib.const | ||||
| import stringcase | ||||
|  | ||||
| @@ -33,6 +35,8 @@ from .casing import safe_snake_case | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from grpclib._protocols import IProtoMessage | ||||
|     from grpclib.client import Channel | ||||
|     from grpclib.metadata import Deadline | ||||
|  | ||||
| if not (sys.version_info.major == 3 and sys.version_info.minor >= 7): | ||||
|     # Apply backport of datetime.fromisoformat from 3.7 | ||||
| @@ -118,7 +122,11 @@ WIRE_LEN_DELIM_TYPES = [TYPE_STRING, TYPE_BYTES, TYPE_MESSAGE, TYPE_MAP] | ||||
|  | ||||
|  | ||||
| # Protobuf datetimes start at the Unix Epoch in 1970 in UTC. | ||||
| DATETIME_ZERO = datetime(1970, 1, 1, tzinfo=timezone.utc) | ||||
| def datetime_default_gen(): | ||||
|     return datetime(1970, 1, 1, tzinfo=timezone.utc) | ||||
|  | ||||
|  | ||||
| DATETIME_ZERO = datetime_default_gen() | ||||
|  | ||||
|  | ||||
| class Casing(enum.Enum): | ||||
| @@ -426,6 +434,63 @@ def parse_fields(value: bytes) -> Generator[ParsedField, None, None]: | ||||
| T = TypeVar("T", bound="Message") | ||||
|  | ||||
|  | ||||
| class ProtoClassMetadata: | ||||
|     cls: Type["Message"] | ||||
|  | ||||
|     def __init__(self, cls: Type["Message"]): | ||||
|         self.cls = cls | ||||
|         by_field = {} | ||||
|         by_group = {} | ||||
|  | ||||
|         for field in dataclasses.fields(cls): | ||||
|             meta = FieldMetadata.get(field) | ||||
|  | ||||
|             if meta.group: | ||||
|                 # This is part of a one-of group. | ||||
|                 by_field[field.name] = meta.group | ||||
|  | ||||
|                 by_group.setdefault(meta.group, set()).add(field) | ||||
|  | ||||
|         self.oneof_group_by_field = by_field | ||||
|         self.oneof_field_by_group = by_group | ||||
|  | ||||
|         self.init_default_gen() | ||||
|         self.init_cls_by_field() | ||||
|  | ||||
|     def init_default_gen(self): | ||||
|         default_gen = {} | ||||
|  | ||||
|         for field in dataclasses.fields(self.cls): | ||||
|             meta = FieldMetadata.get(field) | ||||
|             default_gen[field.name] = self.cls._get_field_default_gen(field, meta) | ||||
|  | ||||
|         self.default_gen = default_gen | ||||
|  | ||||
|     def init_cls_by_field(self): | ||||
|         field_cls = {} | ||||
|  | ||||
|         for field in dataclasses.fields(self.cls): | ||||
|             meta = FieldMetadata.get(field) | ||||
|             if meta.proto_type == TYPE_MAP: | ||||
|                 assert meta.map_types | ||||
|                 kt = self.cls._cls_for(field, index=0) | ||||
|                 vt = self.cls._cls_for(field, index=1) | ||||
|                 Entry = dataclasses.make_dataclass( | ||||
|                     "Entry", | ||||
|                     [ | ||||
|                         ("key", kt, dataclass_field(1, meta.map_types[0])), | ||||
|                         ("value", vt, dataclass_field(2, meta.map_types[1])), | ||||
|                     ], | ||||
|                     bases=(Message,), | ||||
|                 ) | ||||
|                 field_cls[field.name] = Entry | ||||
|                 field_cls[field.name + ".value"] = vt | ||||
|             else: | ||||
|                 field_cls[field.name] = self.cls._cls_for(field) | ||||
|  | ||||
|         self.cls_by_field = field_cls | ||||
|  | ||||
|  | ||||
| class Message(ABC): | ||||
|     """ | ||||
|     A protobuf message base class. Generated code will inherit from this and | ||||
| @@ -443,17 +508,12 @@ class Message(ABC): | ||||
|  | ||||
|         # Set a default value for each field in the class after `__init__` has | ||||
|         # already been run. | ||||
|         group_map: Dict[str, dict] = {"fields": {}, "groups": {}} | ||||
|         group_map: Dict[str, dataclasses.Field] = {} | ||||
|         for field in dataclasses.fields(self): | ||||
|             meta = FieldMetadata.get(field) | ||||
|  | ||||
|             if meta.group: | ||||
|                 # This is part of a one-of group. | ||||
|                 group_map["fields"][field.name] = meta.group | ||||
|  | ||||
|                 if meta.group not in group_map["groups"]: | ||||
|                     group_map["groups"][meta.group] = {"current": None, "fields": set()} | ||||
|                 group_map["groups"][meta.group]["fields"].add(field) | ||||
|                 group_map.setdefault(meta.group) | ||||
|  | ||||
|             if getattr(self, field.name) != PLACEHOLDER: | ||||
|                 # Skip anything not set to the sentinel value | ||||
| @@ -461,7 +521,7 @@ class Message(ABC): | ||||
|  | ||||
|                 if meta.group: | ||||
|                     # This was set, so make it the selected value of the one-of. | ||||
|                     group_map["groups"][meta.group]["current"] = field | ||||
|                     group_map[meta.group] = field | ||||
|  | ||||
|                 continue | ||||
|  | ||||
| @@ -477,19 +537,33 @@ class Message(ABC): | ||||
|             # Track when a field has been set. | ||||
|             self.__dict__["_serialized_on_wire"] = True | ||||
|  | ||||
|         if attr in getattr(self, "_group_map", {}).get("fields", {}): | ||||
|             group = self._group_map["fields"][attr] | ||||
|             for field in self._group_map["groups"][group]["fields"]: | ||||
|                 if field.name == attr: | ||||
|                     self._group_map["groups"][group]["current"] = field | ||||
|                 else: | ||||
|                     super().__setattr__( | ||||
|                         field.name, | ||||
|                         self._get_field_default(field, FieldMetadata.get(field)), | ||||
|                     ) | ||||
|         if hasattr(self, "_group_map"):  # __post_init__ had already run | ||||
|             if attr in self._betterproto.oneof_group_by_field: | ||||
|                 group = self._betterproto.oneof_group_by_field[attr] | ||||
|                 for field in self._betterproto.oneof_field_by_group[group]: | ||||
|                     if field.name == attr: | ||||
|                         self._group_map[group] = field | ||||
|                     else: | ||||
|                         super().__setattr__( | ||||
|                             field.name, | ||||
|                             self._get_field_default(field, FieldMetadata.get(field)), | ||||
|                         ) | ||||
|  | ||||
|         super().__setattr__(attr, value) | ||||
|  | ||||
|     @property | ||||
|     def _betterproto(self): | ||||
|         """ | ||||
|         Lazy initialize metadata for each protobuf class. | ||||
|         It may be initialized multiple times in a multi-threaded environment, | ||||
|         but that won't affect the correctness. | ||||
|         """ | ||||
|         meta = getattr(self.__class__, "_betterproto_meta", None) | ||||
|         if not meta: | ||||
|             meta = ProtoClassMetadata(self.__class__) | ||||
|             self.__class__._betterproto_meta = meta | ||||
|         return meta | ||||
|  | ||||
|     def __bytes__(self) -> bytes: | ||||
|         """ | ||||
|         Get the binary encoded Protobuf representation of this instance. | ||||
| @@ -508,7 +582,7 @@ class Message(ABC): | ||||
|             # currently set in a `oneof` group, so it must be serialized even | ||||
|             # if the value is the default zero value. | ||||
|             selected_in_group = False | ||||
|             if meta.group and self._group_map["groups"][meta.group]["current"] == field: | ||||
|             if meta.group and self._group_map[meta.group] == field: | ||||
|                 selected_in_group = True | ||||
|  | ||||
|             serialize_empty = False | ||||
| @@ -560,47 +634,52 @@ class Message(ABC): | ||||
|     # For compatibility with other libraries | ||||
|     SerializeToString = __bytes__ | ||||
|  | ||||
|     def _type_hint(self, field_name: str) -> Type: | ||||
|         module = inspect.getmodule(self.__class__) | ||||
|         type_hints = get_type_hints(self.__class__, vars(module)) | ||||
|     @classmethod | ||||
|     def _type_hint(cls, field_name: str) -> Type: | ||||
|         module = inspect.getmodule(cls) | ||||
|         type_hints = get_type_hints(cls, vars(module)) | ||||
|         return type_hints[field_name] | ||||
|  | ||||
|     def _cls_for(self, field: dataclasses.Field, index: int = 0) -> Type: | ||||
|     @classmethod | ||||
|     def _cls_for(cls, field: dataclasses.Field, index: int = 0) -> Type: | ||||
|         """Get the message class for a field from the type hints.""" | ||||
|         cls = self._type_hint(field.name) | ||||
|         if hasattr(cls, "__args__") and index >= 0: | ||||
|             cls = cls.__args__[index] | ||||
|         return cls | ||||
|         field_cls = cls._type_hint(field.name) | ||||
|         if hasattr(field_cls, "__args__") and index >= 0: | ||||
|             field_cls = field_cls.__args__[index] | ||||
|         return field_cls | ||||
|  | ||||
|     def _get_field_default(self, field: dataclasses.Field, meta: FieldMetadata) -> Any: | ||||
|         t = self._type_hint(field.name) | ||||
|         return self._betterproto.default_gen[field.name]() | ||||
|  | ||||
|     @classmethod | ||||
|     def _get_field_default_gen( | ||||
|         cls, field: dataclasses.Field, meta: FieldMetadata | ||||
|     ) -> Any: | ||||
|         t = cls._type_hint(field.name) | ||||
|  | ||||
|         value: Any = 0 | ||||
|         if hasattr(t, "__origin__"): | ||||
|             if t.__origin__ in (dict, Dict): | ||||
|                 # This is some kind of map (dict in Python). | ||||
|                 value = {} | ||||
|                 return dict | ||||
|             elif t.__origin__ in (list, List): | ||||
|                 # This is some kind of list (repeated) field. | ||||
|                 value = [] | ||||
|                 return list | ||||
|             elif t.__origin__ == Union and t.__args__[1] == type(None): | ||||
|                 # This is an optional (wrapped) field. For setting the default we | ||||
|                 # really don't care what kind of field it is. | ||||
|                 value = None | ||||
|                 return type(None) | ||||
|             else: | ||||
|                 value = t() | ||||
|                 return t | ||||
|         elif issubclass(t, Enum): | ||||
|             # Enums always default to zero. | ||||
|             value = 0 | ||||
|             return int | ||||
|         elif t == datetime: | ||||
|             # Offsets are relative to 1970-01-01T00:00:00Z | ||||
|             value = DATETIME_ZERO | ||||
|             return datetime_default_gen | ||||
|         else: | ||||
|             # This is either a primitive scalar or another message type. Calling | ||||
|             # it should result in its zero value. | ||||
|             value = t() | ||||
|  | ||||
|         return value | ||||
|             return t | ||||
|  | ||||
|     def _postprocess_single( | ||||
|         self, wire_type: int, meta: FieldMetadata, field: dataclasses.Field, value: Any | ||||
| @@ -625,7 +704,7 @@ class Message(ABC): | ||||
|             if meta.proto_type == TYPE_STRING: | ||||
|                 value = value.decode("utf-8") | ||||
|             elif meta.proto_type == TYPE_MESSAGE: | ||||
|                 cls = self._cls_for(field) | ||||
|                 cls = self._betterproto.cls_by_field[field.name] | ||||
|  | ||||
|                 if cls == datetime: | ||||
|                     value = _Timestamp().parse(value).to_datetime() | ||||
| @@ -639,20 +718,7 @@ class Message(ABC): | ||||
|                     value = cls().parse(value) | ||||
|                     value._serialized_on_wire = True | ||||
|             elif meta.proto_type == TYPE_MAP: | ||||
|                 # TODO: This is slow, use a cache to make it faster since each | ||||
|                 #       key/value pair will recreate the class. | ||||
|                 assert meta.map_types | ||||
|                 kt = self._cls_for(field, index=0) | ||||
|                 vt = self._cls_for(field, index=1) | ||||
|                 Entry = dataclasses.make_dataclass( | ||||
|                     "Entry", | ||||
|                     [ | ||||
|                         ("key", kt, dataclass_field(1, meta.map_types[0])), | ||||
|                         ("value", vt, dataclass_field(2, meta.map_types[1])), | ||||
|                     ], | ||||
|                     bases=(Message,), | ||||
|                 ) | ||||
|                 value = Entry().parse(value) | ||||
|                 value = self._betterproto.cls_by_field[field.name]().parse(value) | ||||
|  | ||||
|         return value | ||||
|  | ||||
| @@ -767,7 +833,9 @@ class Message(ABC): | ||||
|                     else: | ||||
|                         output[cased_name] = b64encode(v).decode("utf8") | ||||
|                 elif meta.proto_type == TYPE_ENUM: | ||||
|                     enum_values = list(self._cls_for(field))  # type: ignore | ||||
|                     enum_values = list( | ||||
|                         self._betterproto.cls_by_field[field.name] | ||||
|                     )  # type: ignore | ||||
|                     if isinstance(v, list): | ||||
|                         output[cased_name] = [enum_values[e].name for e in v] | ||||
|                     else: | ||||
| @@ -793,7 +861,7 @@ class Message(ABC): | ||||
|                     if meta.proto_type == "message": | ||||
|                         v = getattr(self, field.name) | ||||
|                         if isinstance(v, list): | ||||
|                             cls = self._cls_for(field) | ||||
|                             cls = self._betterproto.cls_by_field[field.name] | ||||
|                             for i in range(len(value[key])): | ||||
|                                 v.append(cls().from_dict(value[key][i])) | ||||
|                         elif isinstance(v, datetime): | ||||
| @@ -810,7 +878,7 @@ class Message(ABC): | ||||
|                             v.from_dict(value[key]) | ||||
|                     elif meta.map_types and meta.map_types[1] == TYPE_MESSAGE: | ||||
|                         v = getattr(self, field.name) | ||||
|                         cls = self._cls_for(field, index=1) | ||||
|                         cls = self._betterproto.cls_by_field[field.name + ".value"] | ||||
|                         for k in value[key]: | ||||
|                             v[k] = cls().from_dict(value[key][k]) | ||||
|                     else: | ||||
| @@ -826,7 +894,7 @@ class Message(ABC): | ||||
|                             else: | ||||
|                                 v = b64decode(value[key]) | ||||
|                         elif meta.proto_type == TYPE_ENUM: | ||||
|                             enum_cls = self._cls_for(field) | ||||
|                             enum_cls = self._betterproto.cls_by_field[field.name] | ||||
|                             if isinstance(v, list): | ||||
|                                 v = [enum_cls.from_string(e) for e in v] | ||||
|                             elif isinstance(v, str): | ||||
| @@ -859,7 +927,7 @@ def serialized_on_wire(message: Message) -> bool: | ||||
|  | ||||
| def which_one_of(message: Message, group_name: str) -> Tuple[str, Any]: | ||||
|     """Return the name and value of a message's one-of field group.""" | ||||
|     field = message._group_map["groups"].get(group_name, {}).get("current") | ||||
|     field = message._group_map.get(group_name) | ||||
|     if not field: | ||||
|         return ("", None) | ||||
|     return (field.name, getattr(message, field.name)) | ||||
| @@ -1000,20 +1068,57 @@ def _get_wrapper(proto_type: str) -> Type: | ||||
|     }[proto_type] | ||||
|  | ||||
|  | ||||
| _Value = Union[str, bytes] | ||||
| _MetadataLike = Union[Mapping[str, _Value], Collection[Tuple[str, _Value]]] | ||||
|  | ||||
|  | ||||
| class ServiceStub(ABC): | ||||
|     """ | ||||
|     Base class for async gRPC service stubs. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, channel: grpclib.client.Channel) -> None: | ||||
|     def __init__( | ||||
|         self, | ||||
|         channel: "Channel", | ||||
|         *, | ||||
|         timeout: Optional[float] = None, | ||||
|         deadline: Optional["Deadline"] = None, | ||||
|         metadata: Optional[_MetadataLike] = None, | ||||
|     ) -> None: | ||||
|         self.channel = channel | ||||
|         self.timeout = timeout | ||||
|         self.deadline = deadline | ||||
|         self.metadata = metadata | ||||
|  | ||||
|     def __resolve_request_kwargs( | ||||
|         self, | ||||
|         timeout: Optional[float], | ||||
|         deadline: Optional["Deadline"], | ||||
|         metadata: Optional[_MetadataLike], | ||||
|     ): | ||||
|         return { | ||||
|             "timeout": self.timeout if timeout is None else timeout, | ||||
|             "deadline": self.deadline if deadline is None else deadline, | ||||
|             "metadata": self.metadata if metadata is None else metadata, | ||||
|         } | ||||
|  | ||||
|     async def _unary_unary( | ||||
|         self, route: str, request: "IProtoMessage", response_type: Type[T] | ||||
|         self, | ||||
|         route: str, | ||||
|         request: "IProtoMessage", | ||||
|         response_type: Type[T], | ||||
|         *, | ||||
|         timeout: Optional[float] = None, | ||||
|         deadline: Optional["Deadline"] = None, | ||||
|         metadata: Optional[_MetadataLike] = None, | ||||
|     ) -> T: | ||||
|         """Make a unary request and return the response.""" | ||||
|         async with self.channel.request( | ||||
|             route, grpclib.const.Cardinality.UNARY_UNARY, type(request), response_type | ||||
|             route, | ||||
|             grpclib.const.Cardinality.UNARY_UNARY, | ||||
|             type(request), | ||||
|             response_type, | ||||
|             **self.__resolve_request_kwargs(timeout, deadline, metadata), | ||||
|         ) as stream: | ||||
|             await stream.send_message(request, end=True) | ||||
|             response = await stream.recv_message() | ||||
| @@ -1021,11 +1126,22 @@ class ServiceStub(ABC): | ||||
|             return response | ||||
|  | ||||
|     async def _unary_stream( | ||||
|         self, route: str, request: "IProtoMessage", response_type: Type[T] | ||||
|         self, | ||||
|         route: str, | ||||
|         request: "IProtoMessage", | ||||
|         response_type: Type[T], | ||||
|         *, | ||||
|         timeout: Optional[float] = None, | ||||
|         deadline: Optional["Deadline"] = None, | ||||
|         metadata: Optional[_MetadataLike] = None, | ||||
|     ) -> AsyncGenerator[T, None]: | ||||
|         """Make a unary request and return the stream response iterator.""" | ||||
|         async with self.channel.request( | ||||
|             route, grpclib.const.Cardinality.UNARY_STREAM, type(request), response_type | ||||
|             route, | ||||
|             grpclib.const.Cardinality.UNARY_STREAM, | ||||
|             type(request), | ||||
|             response_type, | ||||
|             **self.__resolve_request_kwargs(timeout, deadline, metadata), | ||||
|         ) as stream: | ||||
|             await stream.send_message(request, end=True) | ||||
|             async for message in stream: | ||||
|   | ||||
							
								
								
									
										2
									
								
								betterproto/plugin.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								betterproto/plugin.bat
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| @SET plugin_dir=%~dp0 | ||||
| @python %plugin_dir%/plugin.py %* | ||||
| @@ -1,12 +1,11 @@ | ||||
| #!/usr/bin/env python | ||||
|  | ||||
| import itertools | ||||
| import json | ||||
| import os.path | ||||
| import re | ||||
| import sys | ||||
| import textwrap | ||||
| from typing import Any, List, Tuple | ||||
| from collections import defaultdict | ||||
| from typing import Dict, List, Optional, Type | ||||
|  | ||||
| try: | ||||
|     import black | ||||
| @@ -24,44 +23,56 @@ from google.protobuf.descriptor_pb2 import ( | ||||
|     DescriptorProto, | ||||
|     EnumDescriptorProto, | ||||
|     FieldDescriptorProto, | ||||
|     FileDescriptorProto, | ||||
|     ServiceDescriptorProto, | ||||
| ) | ||||
|  | ||||
| from betterproto.casing import safe_snake_case | ||||
|  | ||||
| import google.protobuf.wrappers_pb2 as google_wrappers | ||||
|  | ||||
| WRAPPER_TYPES = { | ||||
|     "google.protobuf.DoubleValue": "float", | ||||
|     "google.protobuf.FloatValue": "float", | ||||
|     "google.protobuf.Int64Value": "int", | ||||
|     "google.protobuf.UInt64Value": "int", | ||||
|     "google.protobuf.Int32Value": "int", | ||||
|     "google.protobuf.UInt32Value": "int", | ||||
|     "google.protobuf.BoolValue": "bool", | ||||
|     "google.protobuf.StringValue": "str", | ||||
|     "google.protobuf.BytesValue": "bytes", | ||||
| } | ||||
| WRAPPER_TYPES: Dict[str, Optional[Type]] = defaultdict( | ||||
|     lambda: None, | ||||
|     { | ||||
|         "google.protobuf.DoubleValue": google_wrappers.DoubleValue, | ||||
|         "google.protobuf.FloatValue": google_wrappers.FloatValue, | ||||
|         "google.protobuf.Int64Value": google_wrappers.Int64Value, | ||||
|         "google.protobuf.UInt64Value": google_wrappers.UInt64Value, | ||||
|         "google.protobuf.Int32Value": google_wrappers.Int32Value, | ||||
|         "google.protobuf.UInt32Value": google_wrappers.UInt32Value, | ||||
|         "google.protobuf.BoolValue": google_wrappers.BoolValue, | ||||
|         "google.protobuf.StringValue": google_wrappers.StringValue, | ||||
|         "google.protobuf.BytesValue": google_wrappers.BytesValue, | ||||
|     }, | ||||
| ) | ||||
|  | ||||
|  | ||||
| def get_ref_type(package: str, imports: set, type_name: str) -> str: | ||||
| def get_ref_type( | ||||
|     package: str, imports: set, type_name: str, unwrap: bool = True | ||||
| ) -> str: | ||||
|     """ | ||||
|     Return a Python type name for a proto type reference. Adds the import if | ||||
|     necessary. | ||||
|     necessary. Unwraps well known type if required. | ||||
|     """ | ||||
|     # If the package name is a blank string, then this should still work | ||||
|     # because by convention packages are lowercase and message/enum types are | ||||
|     # pascal-cased. May require refactoring in the future. | ||||
|     type_name = type_name.lstrip(".") | ||||
|  | ||||
|     if type_name in WRAPPER_TYPES: | ||||
|         return f"Optional[{WRAPPER_TYPES[type_name]}]" | ||||
|     # Check if type is wrapper. | ||||
|     wrapper_class = WRAPPER_TYPES[type_name] | ||||
|  | ||||
|     if type_name == "google.protobuf.Duration": | ||||
|         return "timedelta" | ||||
|     if unwrap: | ||||
|         if wrapper_class: | ||||
|             wrapped_type = type(wrapper_class().value) | ||||
|             return f"Optional[{wrapped_type.__name__}]" | ||||
|  | ||||
|     if type_name == "google.protobuf.Timestamp": | ||||
|         return "datetime" | ||||
|         if type_name == "google.protobuf.Duration": | ||||
|             return "timedelta" | ||||
|  | ||||
|         if type_name == "google.protobuf.Timestamp": | ||||
|             return "datetime" | ||||
|     elif wrapper_class: | ||||
|         imports.add(f"from {wrapper_class.__module__} import {wrapper_class.__name__}") | ||||
|         return f"{wrapper_class.__name__}" | ||||
|  | ||||
|     if type_name.startswith(package): | ||||
|         parts = type_name.lstrip(package).lstrip(".").split(".") | ||||
| @@ -122,7 +133,7 @@ def get_py_zero(type_num: int) -> str: | ||||
|  | ||||
|  | ||||
| def traverse(proto_file): | ||||
|     def _traverse(path, items, prefix = ''): | ||||
|     def _traverse(path, items, prefix=""): | ||||
|         for i, item in enumerate(items): | ||||
|             # Adjust the name since we flatten the heirarchy. | ||||
|             item.name = next_prefix = prefix + item.name | ||||
| @@ -172,7 +183,7 @@ def generate_code(request, response): | ||||
|         lstrip_blocks=True, | ||||
|         loader=jinja2.FileSystemLoader("%s/templates/" % os.path.dirname(__file__)), | ||||
|     ) | ||||
|     template = env.get_template("template.py") | ||||
|     template = env.get_template("template.py.j2") | ||||
|  | ||||
|     output_map = {} | ||||
|     for proto_file in request.proto_file: | ||||
| @@ -379,7 +390,10 @@ def generate_code(request, response): | ||||
|                             ).strip('"'), | ||||
|                             "input_message": input_message, | ||||
|                             "output": get_ref_type( | ||||
|                                 package, output["imports"], method.output_type | ||||
|                                 package, | ||||
|                                 output["imports"], | ||||
|                                 method.output_type, | ||||
|                                 unwrap=False, | ||||
|                             ).strip('"'), | ||||
|                             "client_streaming": method.client_streaming, | ||||
|                             "server_streaming": method.server_streaming, | ||||
|   | ||||
| @@ -63,7 +63,7 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub): | ||||
| 
 | ||||
|     {% endif %} | ||||
|     {% for method in service.methods %} | ||||
|     async def {{ method.py_name }}(self{% if method.input_message and method.input_message.properties %}, *, {% for field in method.input_message.properties %}{{ field.name }}: {% if field.zero == "None" and not field.type.startswith("Optional[") %}Optional[{{ field.type }}]{% else %}{{ field.type }}{% endif %} = {{ field.zero }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}) -> {% if method.server_streaming %}AsyncGenerator[{{ method.output }}, None]{% else %}{{ method.output }}{% endif %}: | ||||
|     async def {{ method.py_name }}(self{% if method.input_message and method.input_message.properties %}, *, {% for field in method.input_message.properties %}{{ field.py_name }}: {% if field.zero == "None" and not field.type.startswith("Optional[") %}Optional[{{ field.type }}]{% else %}{{ field.type }}{% endif %} = {{ field.zero }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}) -> {% if method.server_streaming %}AsyncGenerator[{{ method.output }}, None]{% else %}{{ method.output }}{% endif %}: | ||||
|         {% if method.comment %} | ||||
| {{ method.comment }} | ||||
| 
 | ||||
| @@ -71,10 +71,10 @@ class {{ service.py_name }}Stub(betterproto.ServiceStub): | ||||
|         request = {{ method.input }}() | ||||
|         {% for field in method.input_message.properties %} | ||||
|             {% if field.field_type == 'message' %} | ||||
|         if {{ field.name }} is not None: | ||||
|             request.{{ field.name }} = {{ field.name }} | ||||
|         if {{ field.py_name }} is not None: | ||||
|             request.{{ field.py_name }} = {{ field.py_name }} | ||||
|             {% else %} | ||||
|         request.{{ field.name }} = {{ field.name }} | ||||
|         request.{{ field.py_name }} = {{ field.py_name }} | ||||
|             {% endif %} | ||||
|         {% endfor %} | ||||
| 
 | ||||
							
								
								
									
										90
									
								
								betterproto/tests/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								betterproto/tests/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| # Standard Tests Development Guide | ||||
|  | ||||
| Standard test cases are found in [betterproto/tests/inputs](inputs), where each subdirectory represents a testcase, that is verified in isolation. | ||||
|  | ||||
| ``` | ||||
| inputs/ | ||||
|    bool/ | ||||
|    double/ | ||||
|    int32/ | ||||
|    ... | ||||
| ``` | ||||
|  | ||||
| ## Test case directory structure | ||||
|  | ||||
| Each testcase has a `<name>.proto` file with a message called `Test`, a matching `.json` file and optionally a custom test file called `test_*.py`. | ||||
|  | ||||
| ```bash | ||||
| bool/ | ||||
|   bool.proto | ||||
|   bool.json | ||||
|   test_bool.py  # optional | ||||
| ``` | ||||
|  | ||||
| ### proto | ||||
|  | ||||
| `<name>.proto` — *The protobuf message to test* | ||||
|  | ||||
| ```protobuf | ||||
| syntax = "proto3"; | ||||
|  | ||||
| message Test { | ||||
|     bool value = 1; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| You can add multiple `.proto` files to the test case, as long as one file matches the directory name.  | ||||
|  | ||||
| ### json | ||||
|  | ||||
| `<name>.json` — *Test-data to validate the message with* | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "value": true | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### pytest | ||||
|  | ||||
| `test_<name>.py` — *Custom test to validate specific aspects of the generated class* | ||||
|  | ||||
| ```python | ||||
| from betterproto.tests.output_betterproto.bool.bool import Test | ||||
|  | ||||
| def test_value(): | ||||
|     message = Test() | ||||
|     assert not message.value, "Boolean is False by default" | ||||
| ``` | ||||
|  | ||||
| ## Standard tests | ||||
|  | ||||
| The following tests are automatically executed for all cases: | ||||
|  | ||||
| - [x] Can the generated python code imported? | ||||
| - [x] Can the generated message class be instantiated? | ||||
| - [x] Is the generated code compatible with the Google's `grpc_tools.protoc` implementation? | ||||
|  | ||||
| ## Running the tests | ||||
|  | ||||
| - `pipenv run generate` | ||||
|   This generates | ||||
|   - `betterproto/tests/output_betterproto` — *the plugin generated python classes* | ||||
|   - `betterproto/tests/output_reference` — *reference implementation classes* | ||||
| - `pipenv run test` | ||||
|  | ||||
| ## Intentionally Failing tests | ||||
|  | ||||
| The standard test suite includes tests that fail by intention. These tests document known bugs and missing features that are intended to be corrented in the future. | ||||
|  | ||||
| When running `pytest`, they show up as `x` or  `X` in the test results. | ||||
|  | ||||
| ``` | ||||
| betterproto/tests/test_inputs.py ..x...x..x...x.X........xx........x.....x.......x.xx....x...................... [ 84%] | ||||
| ``` | ||||
|  | ||||
| - `.` — PASSED | ||||
| - `x` — XFAIL: expected failure | ||||
| - `X` — XPASS: expected failure, but still passed | ||||
|  | ||||
| Test cases marked for expected failure are declared in [inputs/xfail.py](inputs.xfail.py) | ||||
| @@ -1,84 +1,91 @@ | ||||
| #!/usr/bin/env python | ||||
| import glob | ||||
| import os | ||||
| import shutil | ||||
| import sys | ||||
| from typing import Set | ||||
|  | ||||
| from betterproto.tests.util import ( | ||||
|     get_directories, | ||||
|     inputs_path, | ||||
|     output_path_betterproto, | ||||
|     output_path_reference, | ||||
|     protoc_plugin, | ||||
|     protoc_reference, | ||||
| ) | ||||
|  | ||||
| # Force pure-python implementation instead of C++, otherwise imports | ||||
| # break things because we can't properly reset the symbol database. | ||||
| os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" | ||||
|  | ||||
| import importlib | ||||
| import json | ||||
| import subprocess | ||||
| import sys | ||||
| from typing import Generator, Tuple | ||||
|  | ||||
| from google.protobuf import symbol_database | ||||
| from google.protobuf.descriptor_pool import DescriptorPool | ||||
| from google.protobuf.json_format import MessageToJson, Parse | ||||
| def clear_directory(path: str): | ||||
|     for file_or_directory in glob.glob(os.path.join(path, "*")): | ||||
|         if os.path.isdir(file_or_directory): | ||||
|             shutil.rmtree(file_or_directory) | ||||
|         else: | ||||
|             os.remove(file_or_directory) | ||||
|  | ||||
|  | ||||
| root = os.path.dirname(os.path.realpath(__file__)) | ||||
| def generate(whitelist: Set[str]): | ||||
|     path_whitelist = {os.path.realpath(e) for e in whitelist if os.path.exists(e)} | ||||
|     name_whitelist = {e for e in whitelist if not os.path.exists(e)} | ||||
|  | ||||
|     test_case_names = set(get_directories(inputs_path)) | ||||
|  | ||||
|     for test_case_name in sorted(test_case_names): | ||||
|         test_case_input_path = os.path.realpath( | ||||
|             os.path.join(inputs_path, test_case_name) | ||||
|         ) | ||||
|  | ||||
|         if ( | ||||
|             whitelist | ||||
|             and test_case_input_path not in path_whitelist | ||||
|             and test_case_name not in name_whitelist | ||||
|         ): | ||||
|             continue | ||||
|  | ||||
|         test_case_output_path_reference = os.path.join( | ||||
|             output_path_reference, test_case_name | ||||
|         ) | ||||
|         test_case_output_path_betterproto = os.path.join( | ||||
|             output_path_betterproto, test_case_name | ||||
|         ) | ||||
|  | ||||
|         print(f"Generating output for {test_case_name}") | ||||
|         os.makedirs(test_case_output_path_reference, exist_ok=True) | ||||
|         os.makedirs(test_case_output_path_betterproto, exist_ok=True) | ||||
|  | ||||
|         clear_directory(test_case_output_path_reference) | ||||
|         clear_directory(test_case_output_path_betterproto) | ||||
|  | ||||
|         protoc_reference(test_case_input_path, test_case_output_path_reference) | ||||
|         protoc_plugin(test_case_input_path, test_case_output_path_betterproto) | ||||
|  | ||||
|  | ||||
| def get_files(end: str) -> Generator[str, None, None]: | ||||
|     for r, dirs, files in os.walk(root): | ||||
|         for filename in [f for f in files if f.endswith(end)]: | ||||
|             yield os.path.join(r, filename) | ||||
| HELP = "\n".join( | ||||
|     [ | ||||
|         "Usage: python generate.py", | ||||
|         "       python generate.py [DIRECTORIES or NAMES]", | ||||
|         "Generate python classes for standard tests.", | ||||
|         "", | ||||
|         "DIRECTORIES    One or more relative or absolute directories of test-cases to generate classes for.", | ||||
|         "               python generate.py inputs/bool inputs/double inputs/enum", | ||||
|         "", | ||||
|         "NAMES          One or more test-case names to generate classes for.", | ||||
|         "               python generate.py bool double enums", | ||||
|     ] | ||||
| ) | ||||
|  | ||||
|  | ||||
| def get_base(filename: str) -> str: | ||||
|     return os.path.splitext(os.path.basename(filename))[0] | ||||
| def main(): | ||||
|     if set(sys.argv).intersection({"-h", "--help"}): | ||||
|         print(HELP) | ||||
|         return | ||||
|     whitelist = set(sys.argv[1:]) | ||||
|  | ||||
|  | ||||
| def ensure_ext(filename: str, ext: str) -> str: | ||||
|     if not filename.endswith(ext): | ||||
|         return filename + ext | ||||
|     return filename | ||||
|     generate(whitelist) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     os.chdir(root) | ||||
|  | ||||
|     if len(sys.argv) > 1: | ||||
|         proto_files = [ensure_ext(f, ".proto") for f in sys.argv[1:]] | ||||
|         bases = {get_base(f) for f in proto_files} | ||||
|         json_files = [ | ||||
|             f for f in get_files(".json") if get_base(f).split("-")[0] in bases | ||||
|         ] | ||||
|     else: | ||||
|         proto_files = get_files(".proto") | ||||
|         json_files = get_files(".json") | ||||
|  | ||||
|     for filename in proto_files: | ||||
|         print(f"Generating code for {os.path.basename(filename)}") | ||||
|         subprocess.run( | ||||
|             f"protoc --python_out=. {os.path.basename(filename)}", shell=True | ||||
|         ) | ||||
|         subprocess.run( | ||||
|             f"protoc --plugin=protoc-gen-custom=../plugin.py --custom_out=. {os.path.basename(filename)}", | ||||
|             shell=True, | ||||
|         ) | ||||
|  | ||||
|     for filename in json_files: | ||||
|         # Reset the internal symbol database so we can import the `Test` message | ||||
|         # multiple times. Ugh. | ||||
|         sym = symbol_database.Default() | ||||
|         sym.pool = DescriptorPool() | ||||
|  | ||||
|         parts = get_base(filename).split("-") | ||||
|         out = filename.replace(".json", ".bin") | ||||
|         print(f"Using {parts[0]}_pb2 to generate {os.path.basename(out)}") | ||||
|  | ||||
|         imported = importlib.import_module(f"{parts[0]}_pb2") | ||||
|         input_json = open(filename).read() | ||||
|         parsed = Parse(input_json, imported.Test()) | ||||
|         serialized = parsed.SerializeToString() | ||||
|         preserve = "casing" not in filename | ||||
|         serialized_json = MessageToJson(parsed, preserving_proto_field_name=preserve) | ||||
|  | ||||
|         s_loaded = json.loads(serialized_json) | ||||
|         in_loaded = json.loads(input_json) | ||||
|  | ||||
|         if s_loaded != in_loaded: | ||||
|             raise AssertionError("Expected JSON to be equal:", s_loaded, in_loaded) | ||||
|  | ||||
|         open(out, "wb").write(serialized) | ||||
|     main() | ||||
|   | ||||
							
								
								
									
										6
									
								
								betterproto/tests/inputs/bool/test_bool.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								betterproto/tests/inputs/bool/test_bool.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| from betterproto.tests.output_betterproto.bool.bool import Test | ||||
|  | ||||
|  | ||||
| def test_value(): | ||||
|     message = Test() | ||||
|     assert not message.value, "Boolean is False by default" | ||||
| @@ -9,4 +9,9 @@ enum my_enum { | ||||
| message Test { | ||||
|   int32 camelCase = 1; | ||||
|   my_enum snake_case = 2; | ||||
|   snake_case_message snake_case_message = 3; | ||||
| } | ||||
| 
 | ||||
| message snake_case_message { | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										22
									
								
								betterproto/tests/inputs/casing/test_casing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								betterproto/tests/inputs/casing/test_casing.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import betterproto.tests.output_betterproto.casing.casing as casing | ||||
| from betterproto.tests.output_betterproto.casing.casing import Test | ||||
|  | ||||
|  | ||||
| def test_message_attributes(): | ||||
|     message = Test() | ||||
|     assert hasattr( | ||||
|         message, "snake_case_message" | ||||
|     ), "snake_case field name is same in python" | ||||
|     assert hasattr(message, "camel_case"), "CamelCase field is snake_case in python" | ||||
|  | ||||
|  | ||||
| def test_message_casing(): | ||||
|     assert hasattr( | ||||
|         casing, "SnakeCaseMessage" | ||||
|     ), "snake_case Message name is converted to CamelCase in python" | ||||
|  | ||||
|  | ||||
| def test_enum_casing(): | ||||
|     assert hasattr( | ||||
|         casing, "MyEnum" | ||||
|     ), "snake_case Enum name is converted to CamelCase in python" | ||||
| @@ -0,0 +1,21 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| import "google/protobuf/wrappers.proto"; | ||||
|  | ||||
| // Tests that wrapped values can be used directly as return values | ||||
|  | ||||
| service Test { | ||||
|     rpc GetDouble (Input) returns (google.protobuf.DoubleValue); | ||||
|     rpc GetFloat (Input) returns (google.protobuf.FloatValue); | ||||
|     rpc GetInt64 (Input) returns (google.protobuf.Int64Value); | ||||
|     rpc GetUInt64 (Input) returns (google.protobuf.UInt64Value); | ||||
|     rpc GetInt32 (Input) returns (google.protobuf.Int32Value); | ||||
|     rpc GetUInt32 (Input) returns (google.protobuf.UInt32Value); | ||||
|     rpc GetBool (Input) returns (google.protobuf.BoolValue); | ||||
|     rpc GetString (Input) returns (google.protobuf.StringValue); | ||||
|     rpc GetBytes (Input) returns (google.protobuf.BytesValue); | ||||
| } | ||||
|  | ||||
| message Input { | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,56 @@ | ||||
| from typing import Any, Callable, Optional | ||||
|  | ||||
| import google.protobuf.wrappers_pb2 as wrappers | ||||
| import pytest | ||||
|  | ||||
| from betterproto.tests.mocks import MockChannel | ||||
| from betterproto.tests.output_betterproto.googletypes_response.googletypes_response import ( | ||||
|     TestStub, | ||||
| ) | ||||
|  | ||||
| test_cases = [ | ||||
|     (TestStub.get_double, wrappers.DoubleValue, 2.5), | ||||
|     (TestStub.get_float, wrappers.FloatValue, 2.5), | ||||
|     (TestStub.get_int64, wrappers.Int64Value, -64), | ||||
|     (TestStub.get_u_int64, wrappers.UInt64Value, 64), | ||||
|     (TestStub.get_int32, wrappers.Int32Value, -32), | ||||
|     (TestStub.get_u_int32, wrappers.UInt32Value, 32), | ||||
|     (TestStub.get_bool, wrappers.BoolValue, True), | ||||
|     (TestStub.get_string, wrappers.StringValue, "string"), | ||||
|     (TestStub.get_bytes, wrappers.BytesValue, bytes(0xFF)[0:4]), | ||||
| ] | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases) | ||||
| async def test_channel_receives_wrapped_type( | ||||
|     service_method: Callable[[TestStub], Any], wrapper_class: Callable, value | ||||
| ): | ||||
|     wrapped_value = wrapper_class() | ||||
|     wrapped_value.value = value | ||||
|     channel = MockChannel(responses=[wrapped_value]) | ||||
|     service = TestStub(channel) | ||||
|  | ||||
|     await service_method(service) | ||||
|  | ||||
|     assert channel.requests[0]["response_type"] != Optional[type(value)] | ||||
|     assert channel.requests[0]["response_type"] == type(wrapped_value) | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @pytest.mark.xfail | ||||
| @pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases) | ||||
| async def test_service_unwraps_response( | ||||
|     service_method: Callable[[TestStub], Any], wrapper_class: Callable, value | ||||
| ): | ||||
|     """ | ||||
|     grpclib does not unwrap wrapper values returned by services | ||||
|     """ | ||||
|     wrapped_value = wrapper_class() | ||||
|     wrapped_value.value = value | ||||
|     service = TestStub(MockChannel(responses=[wrapped_value])) | ||||
|  | ||||
|     response_value = await service_method(service) | ||||
|  | ||||
|     assert response_value == value | ||||
|     assert type(response_value) == type(value) | ||||
| @@ -0,0 +1,24 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| import "google/protobuf/wrappers.proto"; | ||||
|  | ||||
| // Tests that wrapped values are supported as part of output message | ||||
| service Test { | ||||
|     rpc getOutput (Input) returns (Output); | ||||
| } | ||||
|  | ||||
| message Input { | ||||
|  | ||||
| } | ||||
|  | ||||
| message Output { | ||||
|     google.protobuf.DoubleValue double_value = 1; | ||||
|     google.protobuf.FloatValue float_value = 2; | ||||
|     google.protobuf.Int64Value int64_value = 3; | ||||
|     google.protobuf.UInt64Value uint64_value = 4; | ||||
|     google.protobuf.Int32Value int32_value = 5; | ||||
|     google.protobuf.UInt32Value uint32_value = 6; | ||||
|     google.protobuf.BoolValue bool_value = 7; | ||||
|     google.protobuf.StringValue string_value = 8; | ||||
|     google.protobuf.BytesValue bytes_value = 9; | ||||
| } | ||||
| @@ -0,0 +1,39 @@ | ||||
| import pytest | ||||
|  | ||||
| from betterproto.tests.mocks import MockChannel | ||||
| from betterproto.tests.output_betterproto.googletypes_response_embedded.googletypes_response_embedded import ( | ||||
|     Output, | ||||
|     TestStub, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_service_passes_through_unwrapped_values_embedded_in_response(): | ||||
|     """ | ||||
|     We do not not need to implement value unwrapping for embedded well-known types, | ||||
|     as this is already handled by grpclib. This test merely shows that this is the case. | ||||
|     """ | ||||
|     output = Output( | ||||
|         double_value=10.0, | ||||
|         float_value=12.0, | ||||
|         int64_value=-13, | ||||
|         uint64_value=14, | ||||
|         int32_value=-15, | ||||
|         uint32_value=16, | ||||
|         bool_value=True, | ||||
|         string_value="string", | ||||
|         bytes_value=bytes(0xFF)[0:4], | ||||
|     ) | ||||
|  | ||||
|     service = TestStub(MockChannel(responses=[output])) | ||||
|     response = await service.get_output() | ||||
|  | ||||
|     assert response.double_value == 10.0 | ||||
|     assert response.float_value == 12.0 | ||||
|     assert response.int64_value == -13 | ||||
|     assert response.uint64_value == 14 | ||||
|     assert response.int32_value == -15 | ||||
|     assert response.uint32_value == 16 | ||||
|     assert response.bool_value | ||||
|     assert response.string_value == "string" | ||||
|     assert response.bytes_value == bytes(0xFF)[0:4] | ||||
| @@ -0,0 +1,7 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| package package.childpackage; | ||||
|  | ||||
| message ChildMessage { | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| import "package_message.proto"; | ||||
|  | ||||
| // Tests generated imports when a message in a package refers to a message in a nested child package. | ||||
|  | ||||
| message Test { | ||||
|   package.PackageMessage message = 1; | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| import "child.proto"; | ||||
|  | ||||
| package package; | ||||
|  | ||||
| message PackageMessage { | ||||
|     package.childpackage.ChildMessage c = 1; | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| package childpackage; | ||||
|  | ||||
| message Message { | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| import "child.proto"; | ||||
|  | ||||
| // Tests generated imports when a message in root refers to a message in a child package. | ||||
|  | ||||
| message Test { | ||||
|   childpackage.Message child = 1; | ||||
| } | ||||
| @@ -0,0 +1,28 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| import "root.proto"; | ||||
| import "other.proto"; | ||||
|  | ||||
| // This test-case verifies that future implementations will support circular dependencies in the generated python files. | ||||
| // | ||||
| // This becomes important when generating 1 python file/module per package, rather than 1 file per proto file. | ||||
| // | ||||
| // Scenario: | ||||
| // | ||||
| //     The proto messages depend on each other in a non-circular way: | ||||
| // | ||||
| //           Test -------> RootPackageMessage <--------------. | ||||
| //             `------------------------------------> OtherPackageMessage | ||||
| // | ||||
| //     Test and RootPackageMessage are in different files, but belong to the same package (root): | ||||
| // | ||||
| //           (Test -------> RootPackageMessage) <------------. | ||||
| //             `------------------------------------> OtherPackageMessage | ||||
| // | ||||
| //     After grouping the packages into single files or modules, a circular dependency is created: | ||||
| // | ||||
| //           (root: Test & RootPackageMessage) <-------> (other: OtherPackageMessage) | ||||
| message Test { | ||||
|   RootPackageMessage message = 1; | ||||
|   other.OtherPackageMessage other =2; | ||||
| } | ||||
| @@ -0,0 +1,8 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| import "root.proto"; | ||||
| package other; | ||||
|  | ||||
| message OtherPackageMessage { | ||||
|     RootPackageMessage rootPackageMessage = 1; | ||||
| } | ||||
| @@ -0,0 +1,5 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| message RootPackageMessage { | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| import "parent_package_message.proto"; | ||||
|  | ||||
| package parent.child; | ||||
|  | ||||
| // Tests generated imports when a message refers to a message defined in its parent package | ||||
|  | ||||
| message Test { | ||||
|   ParentPackageMessage message_implicit = 1; | ||||
|   parent.ParentPackageMessage message_explicit = 2; | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| package parent; | ||||
|  | ||||
| message ParentPackageMessage { | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| import "root.proto"; | ||||
|  | ||||
| package child; | ||||
|  | ||||
| // Tests generated imports when a message inside a child-package refers to a message defined in the root. | ||||
|  | ||||
| message Test { | ||||
|   RootMessage message = 1; | ||||
| } | ||||
| @@ -0,0 +1,5 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
|  | ||||
| message RootMessage { | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| import "sibling.proto"; | ||||
|  | ||||
| // Tests generated imports when a message in the root package refers to another message in the root package | ||||
|  | ||||
| message Test { | ||||
|   SiblingMessage sibling = 1; | ||||
| } | ||||
| @@ -0,0 +1,5 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| message SiblingMessage { | ||||
|  | ||||
| } | ||||
							
								
								
									
										4
									
								
								betterproto/tests/inputs/int32/int32.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								betterproto/tests/inputs/int32/int32.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| { | ||||
|   "positive": 150, | ||||
|   "negative": -150 | ||||
| } | ||||
| @@ -3,5 +3,6 @@ syntax = "proto3"; | ||||
| // Some documentation about the Test message. | ||||
| message Test { | ||||
|     // Some documentation about the count. | ||||
|     int32 count = 1; | ||||
|     int32 positive = 1; | ||||
|     int32 negative = 2; | ||||
| } | ||||
| @@ -5,3 +5,7 @@ message Test { | ||||
|   int32 with = 2; | ||||
|   int32 as = 3; | ||||
| } | ||||
| 
 | ||||
| service TestService { | ||||
|   rpc GetTest(Test) returns (Test) {} | ||||
| } | ||||
							
								
								
									
										3
									
								
								betterproto/tests/inputs/oneof/oneof-name.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								betterproto/tests/inputs/oneof/oneof-name.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "name": "foobar" | ||||
| } | ||||
							
								
								
									
										3
									
								
								betterproto/tests/inputs/oneof/oneof.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								betterproto/tests/inputs/oneof/oneof.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "count": 100 | ||||
| } | ||||
							
								
								
									
										15
									
								
								betterproto/tests/inputs/oneof/test_oneof.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								betterproto/tests/inputs/oneof/test_oneof.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import betterproto | ||||
| from betterproto.tests.output_betterproto.oneof.oneof import Test | ||||
| from betterproto.tests.util import get_test_case_json_data | ||||
|  | ||||
|  | ||||
| def test_which_count(): | ||||
|     message = Test() | ||||
|     message.from_json(get_test_case_json_data("oneof")) | ||||
|     assert betterproto.which_one_of(message, "foo") == ("count", 100) | ||||
|  | ||||
|  | ||||
| def test_which_name(): | ||||
|     message = Test() | ||||
|     message.from_json(get_test_case_json_data("oneof", "oneof-name.json")) | ||||
|     assert betterproto.which_one_of(message, "foo") == ("name", "foobar") | ||||
| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "signal": "PASS" | ||||
| } | ||||
| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "signal": "RESIGN" | ||||
| } | ||||
							
								
								
									
										6
									
								
								betterproto/tests/inputs/oneof_enum/oneof_enum.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								betterproto/tests/inputs/oneof_enum/oneof_enum.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| { | ||||
|   "move": { | ||||
|     "x": 2, | ||||
|     "y": 3 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										18
									
								
								betterproto/tests/inputs/oneof_enum/oneof_enum.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								betterproto/tests/inputs/oneof_enum/oneof_enum.proto
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| message Test { | ||||
|   oneof action { | ||||
|     Signal signal = 1; | ||||
|     Move move = 2; | ||||
|   } | ||||
| } | ||||
|  | ||||
| enum Signal { | ||||
|   PASS = 0; | ||||
|   RESIGN = 1; | ||||
| } | ||||
|  | ||||
| message Move { | ||||
|   int32 x = 1; | ||||
|   int32 y = 2; | ||||
| } | ||||
							
								
								
									
										42
									
								
								betterproto/tests/inputs/oneof_enum/test_oneof_enum.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								betterproto/tests/inputs/oneof_enum/test_oneof_enum.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import pytest | ||||
|  | ||||
| import betterproto | ||||
| from betterproto.tests.output_betterproto.oneof_enum.oneof_enum import ( | ||||
|     Move, | ||||
|     Signal, | ||||
|     Test, | ||||
| ) | ||||
| from betterproto.tests.util import get_test_case_json_data | ||||
|  | ||||
|  | ||||
| @pytest.mark.xfail | ||||
| def test_which_one_of_returns_enum_with_default_value(): | ||||
|     """ | ||||
|     returns first field when it is enum and set with default value | ||||
|     """ | ||||
|     message = Test() | ||||
|     message.from_json(get_test_case_json_data("oneof_enum", "oneof_enum-enum-0.json")) | ||||
|     assert message.move is None | ||||
|     assert message.signal == Signal.PASS | ||||
|     assert betterproto.which_one_of(message, "action") == ("signal", Signal.PASS) | ||||
|  | ||||
|  | ||||
| @pytest.mark.xfail | ||||
| def test_which_one_of_returns_enum_with_non_default_value(): | ||||
|     """ | ||||
|     returns first field when it is enum and set with non default value | ||||
|     """ | ||||
|     message = Test() | ||||
|     message.from_json(get_test_case_json_data("oneof_enum", "oneof_enum-enum-1.json")) | ||||
|     assert message.move is None | ||||
|     assert message.signal == Signal.PASS | ||||
|     assert betterproto.which_one_of(message, "action") == ("signal", Signal.RESIGN) | ||||
|  | ||||
|  | ||||
| @pytest.mark.xfail | ||||
| def test_which_one_of_returns_second_field_when_set(): | ||||
|     message = Test() | ||||
|     message.from_json(get_test_case_json_data("oneof_enum")) | ||||
|     assert message.move == Move(x=2, y=3) | ||||
|     assert message.signal == 0 | ||||
|     assert betterproto.which_one_of(message, "action") == ("move", Move(x=2, y=3)) | ||||
| @@ -0,0 +1,11 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| package repeatedmessage; | ||||
|  | ||||
| message Test { | ||||
|   repeated Sub greetings = 1; | ||||
| } | ||||
|  | ||||
| message Sub { | ||||
|   string greeting = 1; | ||||
| } | ||||
							
								
								
									
										15
									
								
								betterproto/tests/inputs/service/service.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								betterproto/tests/inputs/service/service.proto
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| package service; | ||||
|  | ||||
| message DoThingRequest { | ||||
|   int32 iterations = 1; | ||||
| } | ||||
|  | ||||
| message DoThingResponse { | ||||
|   int32 successfulIterations = 1; | ||||
| } | ||||
|  | ||||
| service Test { | ||||
|   rpc DoThing (DoThingRequest) returns (DoThingResponse); | ||||
| } | ||||
							
								
								
									
										132
									
								
								betterproto/tests/inputs/service/test_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								betterproto/tests/inputs/service/test_service.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| import betterproto | ||||
| import grpclib | ||||
| from grpclib.testing import ChannelFor | ||||
| import pytest | ||||
| from typing import Dict | ||||
|  | ||||
| from betterproto.tests.output_betterproto.service.service import ( | ||||
|     DoThingResponse, | ||||
|     DoThingRequest, | ||||
|     TestStub as ExampleServiceStub, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class ExampleService: | ||||
|     def __init__(self, test_hook=None): | ||||
|         # This lets us pass assertions to the servicer ;) | ||||
|         self.test_hook = test_hook | ||||
|  | ||||
|     async def DoThing( | ||||
|         self, stream: "grpclib.server.Stream[DoThingRequest, DoThingResponse]" | ||||
|     ): | ||||
|         request = await stream.recv_message() | ||||
|         print("self.test_hook", self.test_hook) | ||||
|         if self.test_hook is not None: | ||||
|             self.test_hook(stream) | ||||
|         for iteration in range(request.iterations): | ||||
|             pass | ||||
|         await stream.send_message(DoThingResponse(request.iterations)) | ||||
|  | ||||
|     def __mapping__(self) -> Dict[str, grpclib.const.Handler]: | ||||
|         return { | ||||
|             "/service.Test/DoThing": grpclib.const.Handler( | ||||
|                 self.DoThing, | ||||
|                 grpclib.const.Cardinality.UNARY_UNARY, | ||||
|                 DoThingRequest, | ||||
|                 DoThingResponse, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|  | ||||
| async def _test_stub(stub, iterations=42, **kwargs): | ||||
|     response = await stub.do_thing(iterations=iterations) | ||||
|     assert response.successful_iterations == iterations | ||||
|  | ||||
|  | ||||
| def _get_server_side_test(deadline, metadata): | ||||
|     def server_side_test(stream): | ||||
|         assert stream.deadline._timestamp == pytest.approx( | ||||
|             deadline._timestamp, 1 | ||||
|         ), "The provided deadline should be recieved serverside" | ||||
|         assert ( | ||||
|             stream.metadata["authorization"] == metadata["authorization"] | ||||
|         ), "The provided authorization metadata should be recieved serverside" | ||||
|  | ||||
|     return server_side_test | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_simple_service_call(): | ||||
|     async with ChannelFor([ExampleService()]) as channel: | ||||
|         await _test_stub(ExampleServiceStub(channel)) | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_service_call_with_upfront_request_params(): | ||||
|     # Setting deadline | ||||
|     deadline = grpclib.metadata.Deadline.from_timeout(22) | ||||
|     metadata = {"authorization": "12345"} | ||||
|     async with ChannelFor( | ||||
|         [ExampleService(test_hook=_get_server_side_test(deadline, metadata))] | ||||
|     ) as channel: | ||||
|         await _test_stub( | ||||
|             ExampleServiceStub(channel, deadline=deadline, metadata=metadata) | ||||
|         ) | ||||
|  | ||||
|     # Setting timeout | ||||
|     timeout = 99 | ||||
|     deadline = grpclib.metadata.Deadline.from_timeout(timeout) | ||||
|     metadata = {"authorization": "12345"} | ||||
|     async with ChannelFor( | ||||
|         [ExampleService(test_hook=_get_server_side_test(deadline, metadata))] | ||||
|     ) as channel: | ||||
|         await _test_stub( | ||||
|             ExampleServiceStub(channel, timeout=timeout, metadata=metadata) | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_service_call_lower_level_with_overrides(): | ||||
|     ITERATIONS = 99 | ||||
|  | ||||
|     # Setting deadline | ||||
|     deadline = grpclib.metadata.Deadline.from_timeout(22) | ||||
|     metadata = {"authorization": "12345"} | ||||
|     kwarg_deadline = grpclib.metadata.Deadline.from_timeout(28) | ||||
|     kwarg_metadata = {"authorization": "12345"} | ||||
|     async with ChannelFor( | ||||
|         [ExampleService(test_hook=_get_server_side_test(deadline, metadata))] | ||||
|     ) as channel: | ||||
|         stub = ExampleServiceStub(channel, deadline=deadline, metadata=metadata) | ||||
|         response = await stub._unary_unary( | ||||
|             "/service.Test/DoThing", | ||||
|             DoThingRequest(ITERATIONS), | ||||
|             DoThingResponse, | ||||
|             deadline=kwarg_deadline, | ||||
|             metadata=kwarg_metadata, | ||||
|         ) | ||||
|         assert response.successful_iterations == ITERATIONS | ||||
|  | ||||
|     # Setting timeout | ||||
|     timeout = 99 | ||||
|     deadline = grpclib.metadata.Deadline.from_timeout(timeout) | ||||
|     metadata = {"authorization": "12345"} | ||||
|     kwarg_timeout = 9000 | ||||
|     kwarg_deadline = grpclib.metadata.Deadline.from_timeout(kwarg_timeout) | ||||
|     kwarg_metadata = {"authorization": "09876"} | ||||
|     async with ChannelFor( | ||||
|         [ | ||||
|             ExampleService( | ||||
|                 test_hook=_get_server_side_test(kwarg_deadline, kwarg_metadata) | ||||
|             ) | ||||
|         ] | ||||
|     ) as channel: | ||||
|         stub = ExampleServiceStub(channel, deadline=deadline, metadata=metadata) | ||||
|         response = await stub._unary_unary( | ||||
|             "/service.Test/DoThing", | ||||
|             DoThingRequest(ITERATIONS), | ||||
|             DoThingResponse, | ||||
|             timeout=kwarg_timeout, | ||||
|             metadata=kwarg_metadata, | ||||
|         ) | ||||
|         assert response.successful_iterations == ITERATIONS | ||||
							
								
								
									
										6
									
								
								betterproto/tests/inputs/signed/signed.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								betterproto/tests/inputs/signed/signed.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| { | ||||
|   "signed32": 150, | ||||
|   "negative32": -150, | ||||
|   "string64": "150", | ||||
|   "negative64": "-150" | ||||
| } | ||||
							
								
								
									
										9
									
								
								betterproto/tests/inputs/signed/signed.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								betterproto/tests/inputs/signed/signed.proto
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| message Test { | ||||
|     // todo: rename fields after fixing bug where 'signed_32_positive' will map to 'signed_32Positive' as output json | ||||
|     sint32 signed32 = 1;    //  signed_32_positive | ||||
|     sint32 negative32 = 2;  //  signed_32_negative | ||||
|     sint64 string64 = 3;    //  signed_64_positive | ||||
|     sint64 negative64 = 4;  //  signed_64_negative | ||||
| } | ||||
							
								
								
									
										10
									
								
								betterproto/tests/inputs/xfail.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								betterproto/tests/inputs/xfail.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| # Test cases that are expected to fail, e.g. unimplemented features or bug-fixes. | ||||
| # Remove from list when fixed. | ||||
| tests = { | ||||
|     "import_root_sibling", | ||||
|     "import_child_package_from_package", | ||||
|     "import_root_package_from_child", | ||||
|     "import_parent_package_from_child", | ||||
|     "import_circular_dependency", | ||||
|     "oneof_enum", | ||||
| } | ||||
| @@ -1,3 +0,0 @@ | ||||
| { | ||||
|   "count": -150 | ||||
| } | ||||
| @@ -1,3 +0,0 @@ | ||||
| { | ||||
|   "count": 150 | ||||
| } | ||||
							
								
								
									
										39
									
								
								betterproto/tests/mocks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								betterproto/tests/mocks.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| from typing import List | ||||
|  | ||||
| from grpclib.client import Channel | ||||
|  | ||||
|  | ||||
| class MockChannel(Channel): | ||||
|     # noinspection PyMissingConstructor | ||||
|     def __init__(self, responses=None) -> None: | ||||
|         self.responses = responses if responses else [] | ||||
|         self.requests = [] | ||||
|  | ||||
|     def request(self, route, cardinality, request, response_type, **kwargs): | ||||
|         self.requests.append( | ||||
|             { | ||||
|                 "route": route, | ||||
|                 "cardinality": cardinality, | ||||
|                 "request": request, | ||||
|                 "response_type": response_type, | ||||
|             } | ||||
|         ) | ||||
|         return MockStream(self.responses) | ||||
|  | ||||
|  | ||||
| class MockStream: | ||||
|     def __init__(self, responses: List) -> None: | ||||
|         super().__init__() | ||||
|         self.responses = responses | ||||
|  | ||||
|     async def recv_message(self): | ||||
|         return self.responses.pop(0) | ||||
|  | ||||
|     async def send_message(self, *args, **kwargs): | ||||
|         pass | ||||
|  | ||||
|     async def __aexit__(self, exc_type, exc_val, exc_tb): | ||||
|         return True | ||||
|  | ||||
|     async def __aenter__(self): | ||||
|         return self | ||||
| @@ -1,3 +0,0 @@ | ||||
| { | ||||
|   "name": "foo" | ||||
| } | ||||
| @@ -1,3 +0,0 @@ | ||||
| { | ||||
|   "count": 1 | ||||
| } | ||||
| @@ -1,4 +0,0 @@ | ||||
| { | ||||
|   "signed_32": -150, | ||||
|   "signed_64": "-150" | ||||
| } | ||||
| @@ -1,4 +0,0 @@ | ||||
| { | ||||
|   "signed_32": 150, | ||||
|   "signed_64": "150" | ||||
| } | ||||
| @@ -1,6 +0,0 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| message Test { | ||||
|     sint32 signed_32 = 1; | ||||
|     sint64 signed_64 = 2; | ||||
| } | ||||
| @@ -256,7 +256,7 @@ def test_to_dict_default_values(): | ||||
|         some_double: float = betterproto.double_field(2) | ||||
|         some_message: TestChildMessage = betterproto.message_field(3) | ||||
|  | ||||
|     test = TestParentMessage().from_dict({"someInt": 0, "someDouble": 1.2,}) | ||||
|     test = TestParentMessage().from_dict({"someInt": 0, "someDouble": 1.2}) | ||||
|  | ||||
|     assert test.to_dict(include_default_values=True) == { | ||||
|         "someInt": 0, | ||||
|   | ||||
| @@ -1,32 +1,147 @@ | ||||
| import importlib | ||||
| import json | ||||
| import os | ||||
| import sys | ||||
| from collections import namedtuple | ||||
| from typing import Set | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from .generate import get_base, get_files | ||||
| import betterproto | ||||
| from betterproto.tests.inputs import xfail | ||||
| from betterproto.tests.mocks import MockChannel | ||||
| from betterproto.tests.util import get_directories, get_test_case_json_data, inputs_path | ||||
|  | ||||
| inputs = get_files(".bin") | ||||
| # Force pure-python implementation instead of C++, otherwise imports | ||||
| # break things because we can't properly reset the symbol database. | ||||
| os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" | ||||
|  | ||||
| from google.protobuf import symbol_database | ||||
| from google.protobuf.descriptor_pool import DescriptorPool | ||||
| from google.protobuf.json_format import Parse | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("filename", inputs) | ||||
| def test_sample(filename: str) -> None: | ||||
|     module = get_base(filename).split("-")[0] | ||||
|     imported = importlib.import_module(f"betterproto.tests.{module}") | ||||
|     data_binary = open(filename, "rb").read() | ||||
|     data_dict = json.loads(open(filename.replace(".bin", ".json")).read()) | ||||
|     t1 = imported.Test().parse(data_binary) | ||||
|     t2 = imported.Test().from_dict(data_dict) | ||||
|     print(t1) | ||||
|     print(t2) | ||||
| class TestCases: | ||||
|     def __init__(self, path, services: Set[str], xfail: Set[str]): | ||||
|         _all = set(get_directories(path)) | ||||
|         _services = services | ||||
|         _messages = _all - services | ||||
|         _messages_with_json = { | ||||
|             test for test in _messages if get_test_case_json_data(test) | ||||
|         } | ||||
|  | ||||
|     # Equality should automagically work for dataclasses! | ||||
|     assert t1 == t2 | ||||
|         self.all = self.apply_xfail_marks(_all, xfail) | ||||
|         self.services = self.apply_xfail_marks(_services, xfail) | ||||
|         self.messages = self.apply_xfail_marks(_messages, xfail) | ||||
|         self.messages_with_json = self.apply_xfail_marks(_messages_with_json, xfail) | ||||
|  | ||||
|     # Generally this can't be relied on, but here we are aiming to match the | ||||
|     # existing Python implementation and aren't doing anything tricky. | ||||
|     # https://developers.google.com/protocol-buffers/docs/encoding#implications | ||||
|     assert bytes(t1) == data_binary | ||||
|     assert bytes(t2) == data_binary | ||||
|     @staticmethod | ||||
|     def apply_xfail_marks(test_set: Set[str], xfail: Set[str]): | ||||
|         return [ | ||||
|             pytest.param(test, marks=pytest.mark.xfail) if test in xfail else test | ||||
|             for test in test_set | ||||
|         ] | ||||
|  | ||||
|     assert t1.to_dict() == data_dict | ||||
|     assert t2.to_dict() == data_dict | ||||
|  | ||||
| test_cases = TestCases( | ||||
|     path=inputs_path, | ||||
|     # test cases for services | ||||
|     services={"googletypes_response", "googletypes_response_embedded", "service"}, | ||||
|     xfail=xfail.tests, | ||||
| ) | ||||
|  | ||||
| plugin_output_package = "betterproto.tests.output_betterproto" | ||||
| reference_output_package = "betterproto.tests.output_reference" | ||||
|  | ||||
|  | ||||
| TestData = namedtuple("TestData", "plugin_module, reference_module, json_data") | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def test_data(request): | ||||
|     test_case_name = request.param | ||||
|  | ||||
|     # Reset the internal symbol database so we can import the `Test` message | ||||
|     # multiple times. Ugh. | ||||
|     sym = symbol_database.Default() | ||||
|     sym.pool = DescriptorPool() | ||||
|  | ||||
|     reference_module_root = os.path.join( | ||||
|         *reference_output_package.split("."), test_case_name | ||||
|     ) | ||||
|  | ||||
|     sys.path.append(reference_module_root) | ||||
|  | ||||
|     yield ( | ||||
|         TestData( | ||||
|             plugin_module=importlib.import_module( | ||||
|                 f"{plugin_output_package}.{test_case_name}.{test_case_name}" | ||||
|             ), | ||||
|             reference_module=lambda: importlib.import_module( | ||||
|                 f"{reference_output_package}.{test_case_name}.{test_case_name}_pb2" | ||||
|             ), | ||||
|             json_data=get_test_case_json_data(test_case_name), | ||||
|         ) | ||||
|     ) | ||||
|  | ||||
|     sys.path.remove(reference_module_root) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("test_data", test_cases.messages, indirect=True) | ||||
| def test_message_can_instantiated(test_data: TestData) -> None: | ||||
|     plugin_module, *_ = test_data | ||||
|     plugin_module.Test() | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("test_data", test_cases.messages, indirect=True) | ||||
| def test_message_equality(test_data: TestData) -> None: | ||||
|     plugin_module, *_ = test_data | ||||
|     message1 = plugin_module.Test() | ||||
|     message2 = plugin_module.Test() | ||||
|     assert message1 == message2 | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("test_data", test_cases.messages_with_json, indirect=True) | ||||
| def test_message_json(repeat, test_data: TestData) -> None: | ||||
|     plugin_module, _, json_data = test_data | ||||
|  | ||||
|     for _ in range(repeat): | ||||
|         message: betterproto.Message = plugin_module.Test() | ||||
|  | ||||
|         message.from_json(json_data) | ||||
|         message_json = message.to_json(0) | ||||
|  | ||||
|         assert json.loads(json_data) == json.loads(message_json) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("test_data", test_cases.services, indirect=True) | ||||
| def test_service_can_be_instantiated(test_data: TestData) -> None: | ||||
|     plugin_module, _, json_data = test_data | ||||
|     plugin_module.TestStub(MockChannel()) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("test_data", test_cases.messages_with_json, indirect=True) | ||||
| def test_binary_compatibility(repeat, test_data: TestData) -> None: | ||||
|     plugin_module, reference_module, json_data = test_data | ||||
|  | ||||
|     reference_instance = Parse(json_data, reference_module().Test()) | ||||
|     reference_binary_output = reference_instance.SerializeToString() | ||||
|  | ||||
|     for _ in range(repeat): | ||||
|         plugin_instance_from_json: betterproto.Message = plugin_module.Test().from_json( | ||||
|             json_data | ||||
|         ) | ||||
|         plugin_instance_from_binary = plugin_module.Test.FromString( | ||||
|             reference_binary_output | ||||
|         ) | ||||
|  | ||||
|         # # Generally this can't be relied on, but here we are aiming to match the | ||||
|         # # existing Python implementation and aren't doing anything tricky. | ||||
|         # # https://developers.google.com/protocol-buffers/docs/encoding#implications | ||||
|         assert bytes(plugin_instance_from_json) == reference_binary_output | ||||
|         assert bytes(plugin_instance_from_binary) == reference_binary_output | ||||
|  | ||||
|         assert plugin_instance_from_json == plugin_instance_from_binary | ||||
|         assert ( | ||||
|             plugin_instance_from_json.to_dict() == plugin_instance_from_binary.to_dict() | ||||
|         ) | ||||
|   | ||||
							
								
								
									
										61
									
								
								betterproto/tests/util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								betterproto/tests/util.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| import os | ||||
| import subprocess | ||||
| from typing import Generator | ||||
|  | ||||
| os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" | ||||
|  | ||||
| root_path = os.path.dirname(os.path.realpath(__file__)) | ||||
| inputs_path = os.path.join(root_path, "inputs") | ||||
| output_path_reference = os.path.join(root_path, "output_reference") | ||||
| output_path_betterproto = os.path.join(root_path, "output_betterproto") | ||||
|  | ||||
| if os.name == "nt": | ||||
|     plugin_path = os.path.join(root_path, "..", "plugin.bat") | ||||
| else: | ||||
|     plugin_path = os.path.join(root_path, "..", "plugin.py") | ||||
|  | ||||
|  | ||||
| def get_files(path, end: str) -> Generator[str, None, None]: | ||||
|     for r, dirs, files in os.walk(path): | ||||
|         for filename in [f for f in files if f.endswith(end)]: | ||||
|             yield os.path.join(r, filename) | ||||
|  | ||||
|  | ||||
| def get_directories(path): | ||||
|     for root, directories, files in os.walk(path): | ||||
|         for directory in directories: | ||||
|             yield directory | ||||
|  | ||||
|  | ||||
| def relative(file: str, path: str): | ||||
|     return os.path.join(os.path.dirname(file), path) | ||||
|  | ||||
|  | ||||
| def read_relative(file: str, path: str): | ||||
|     with open(relative(file, path)) as fh: | ||||
|         return fh.read() | ||||
|  | ||||
|  | ||||
| def protoc_plugin(path: str, output_dir: str): | ||||
|     subprocess.run( | ||||
|         f"protoc --plugin=protoc-gen-custom={plugin_path} --custom_out={output_dir} --proto_path={path} {path}/*.proto", | ||||
|         shell=True, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def protoc_reference(path: str, output_dir: str): | ||||
|     subprocess.run( | ||||
|         f"protoc --python_out={output_dir} --proto_path={path} {path}/*.proto", | ||||
|         shell=True, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def get_test_case_json_data(test_case_name, json_file_name=None): | ||||
|     test_data_file_name = json_file_name if json_file_name else f"{test_case_name}.json" | ||||
|     test_data_file_path = os.path.join(inputs_path, test_case_name, test_data_file_name) | ||||
|  | ||||
|     if not os.path.exists(test_data_file_path): | ||||
|         return None | ||||
|  | ||||
|     with open(test_data_file_path) as fh: | ||||
|         return fh.read() | ||||
							
								
								
									
										12
									
								
								conftest.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								conftest.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import pytest | ||||
|  | ||||
|  | ||||
| def pytest_addoption(parser): | ||||
|     parser.addoption( | ||||
|         "--repeat", type=int, default=1, help="repeat the operation multiple times" | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @pytest.fixture(scope="session") | ||||
| def repeat(request): | ||||
|     return request.config.getoption("repeat") | ||||
							
								
								
									
										5
									
								
								pytest.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								pytest.ini
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| [pytest] | ||||
| python_files = test_*.py | ||||
| python_classes = | ||||
| norecursedirs = **/output_* | ||||
| addopts = -p no:warnings | ||||
							
								
								
									
										6
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								setup.py
									
									
									
									
									
								
							| @@ -2,9 +2,9 @@ from setuptools import setup, find_packages | ||||
|  | ||||
| setup( | ||||
|     name="betterproto", | ||||
|     version="1.2.3", | ||||
|     version="1.2.5", | ||||
|     description="A better Protobuf / gRPC generator & library", | ||||
|     long_description=open("README.md", "r").read(), | ||||
|     long_description=open("README.md", "r", encoding="utf-8").read(), | ||||
|     long_description_content_type="text/markdown", | ||||
|     url="http://github.com/danielgtaylor/python-betterproto", | ||||
|     author="Daniel G. Taylor", | ||||
| @@ -16,7 +16,7 @@ setup( | ||||
|     packages=find_packages( | ||||
|         exclude=["tests", "*.tests", "*.tests.*", "output", "output.*"] | ||||
|     ), | ||||
|     package_data={"betterproto": ["py.typed", "templates/template.py"]}, | ||||
|     package_data={"betterproto": ["py.typed", "templates/template.py.j2"]}, | ||||
|     python_requires=">=3.6", | ||||
|     install_requires=[ | ||||
|         'dataclasses; python_version<"3.7"', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user