diff --git a/noxfile.py b/noxfile.py index 94ce2c31..0e707835 100644 --- a/noxfile.py +++ b/noxfile.py @@ -30,16 +30,11 @@ def unit(session, proto="python"): "py.test", "-W=error", "--quiet", - *( - session.posargs # Coverage info when running individual tests is annoying. - or [ - "--cov=proto", - "--cov-config=.coveragerc", - "--cov-report=term", - "--cov-report=html", - os.path.join("tests", ""), - ] - ), + "--cov=proto", + "--cov-config=.coveragerc", + "--cov-report=term", + "--cov-report=html", + os.path.join("tests", ""), ) diff --git a/proto/_file_info.py b/proto/_file_info.py index 34ec79c5..0c1e74f0 100644 --- a/proto/_file_info.py +++ b/proto/_file_info.py @@ -12,11 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import collections +import collections.abc import inspect import logging -from google.protobuf import descriptor_pb2 from google.protobuf import descriptor_pool from google.protobuf import message from google.protobuf import reflection @@ -28,33 +27,11 @@ class _FileInfo( collections.namedtuple( - "_FileInfo", - ["descriptor", "messages", "enums", "name", "nested", "nested_enum"], + "_FileInfo", ["descriptor", "messages", "enums", "name", "nested"] ) ): registry = {} # Mapping[str, '_FileInfo'] - @classmethod - def maybe_add_descriptor(cls, filename, package): - descriptor = cls.registry.get(filename) - if not descriptor: - descriptor = cls.registry[filename] = cls( - descriptor=descriptor_pb2.FileDescriptorProto( - name=filename, package=package, syntax="proto3", - ), - enums=collections.OrderedDict(), - messages=collections.OrderedDict(), - name=filename, - nested={}, - nested_enum={}, - ) - - return descriptor - - @staticmethod - def proto_file_name(name): - return "{0}.proto".format(name.replace(".", "/")) - def _get_manifest(self, new_class): module = inspect.getmodule(new_class) if hasattr(module, "__protobuf__"): @@ -130,13 +107,6 @@ def generate_file_pb(self, new_class, fallback_salt=""): for field in proto_plus_message._meta.fields.values(): if field.message and isinstance(field.message, str): field.message = self.messages[field.message] - elif field.enum and isinstance(field.enum, str): - field.enum = self.enums[field.enum] - - # Same thing for enums - for full_name, proto_plus_enum in self.enums.items(): - descriptor = pool.FindEnumTypeByName(full_name) - proto_plus_enum._meta.pb = descriptor # We no longer need to track this file's info; remove it from # the module's registry and from this object. @@ -160,16 +130,14 @@ def ready(self, new_class): """ # If there are any nested descriptors that have not been assigned to # the descriptors that should contain them, then we are not ready. - if len(self.nested) or len(self.nested_enum): + if len(self.nested): return False # If there are any unresolved fields (fields with a composite message # declared as a string), ensure that the corresponding message is # declared. for field in self.unresolved_fields: - if (field.message and field.message not in self.messages) or ( - field.enum and field.enum not in self.enums - ): + if field.message not in self.messages: return False # If the module in which this class is defined provides a @@ -188,7 +156,5 @@ def unresolved_fields(self): """Return fields with referencing message types as strings.""" for proto_plus_message in self.messages.values(): for field in proto_plus_message._meta.fields.values(): - if (field.message and isinstance(field.message, str)) or ( - field.enum and isinstance(field.enum, str) - ): + if field.message and isinstance(field.message, str): yield field diff --git a/proto/enums.py b/proto/enums.py index 22e8f42d..ef1af591 100644 --- a/proto/enums.py +++ b/proto/enums.py @@ -14,9 +14,6 @@ import enum -from google.protobuf import descriptor_pb2 - -from proto import _file_info from proto import _package_info from proto.marshal.rules.enums import EnumRule @@ -33,58 +30,12 @@ def __new__(mcls, name, bases, attrs): # this component belongs within the file. package, marshal = _package_info.compile(name, attrs) - # Determine the local path of this proto component within the file. - local_path = tuple(attrs.get("__qualname__", name).split(".")) - - # Sanity check: We get the wrong full name if a class is declared - # inside a function local scope; correct this. - if "" in local_path: - ix = local_path.index("") - local_path = local_path[: ix - 1] + local_path[ix + 1 :] - - # Determine the full name in protocol buffers. - full_name = ".".join((package,) + local_path).lstrip(".") - filename = _file_info._FileInfo.proto_file_name( - attrs.get("__module__", name.lower()) - ) - enum_desc = descriptor_pb2.EnumDescriptorProto( - name=name, - # Note: the superclass ctor removes the variants, so get them now. - # Note: proto3 requires that the first variant value be zero. - value=sorted( - ( - descriptor_pb2.EnumValueDescriptorProto(name=name, number=number) - # Minor hack to get all the enum variants out. - for name, number in attrs.items() - if isinstance(number, int) - ), - key=lambda v: v.number, - ), - ) - - file_info = _file_info._FileInfo.maybe_add_descriptor(filename, package) - if len(local_path) == 1: - file_info.descriptor.enum_type.add().MergeFrom(enum_desc) - else: - file_info.nested_enum[local_path] = enum_desc - # Run the superclass constructor. cls = super().__new__(mcls, name, bases, attrs) - # We can't just add a "_meta" element to attrs because the Enum - # machinery doesn't know what to do with a non-int value. - # The pb is set later, in generate_file_pb - cls._meta = _EnumInfo(full_name=full_name, pb=None) - - file_info.enums[full_name] = cls - # Register the enum with the marshal. marshal.register(cls, EnumRule(cls)) - # Generate the descriptor for the file if it is ready. - if file_info.ready(new_class=cls): - file_info.generate_file_pb(new_class=cls, fallback_salt=full_name) - # Done; return the class. return cls @@ -93,9 +44,3 @@ class Enum(enum.IntEnum, metaclass=ProtoEnumMeta): """A enum object that also builds a protobuf enum descriptor.""" pass - - -class _EnumInfo: - def __init__(self, *, full_name: str, pb): - self.full_name = full_name - self.pb = pb diff --git a/proto/fields.py b/proto/fields.py index f580ef2f..c951a88c 100644 --- a/proto/fields.py +++ b/proto/fields.py @@ -72,6 +72,7 @@ def __init__( def descriptor(self): """Return the descriptor for the field.""" if not self._descriptor: + proto_type = self.proto_type # Resolve the message type, if any, to a string. type_name = None if isinstance(self.message, str): @@ -84,23 +85,29 @@ def descriptor(self): type_name = ( self.message.DESCRIPTOR.full_name if hasattr(self.message, "DESCRIPTOR") - else self.message._meta.full_name + else self.message.meta.full_name ) - elif isinstance(self.enum, str): - if not self.enum.startswith(self.package): - self.enum = "{package}.{name}".format( - package=self.package, name=self.enum, - ) - type_name = self.enum elif self.enum: - type_name = self.enum._meta.full_name + # Nos decipiat. + # + # As far as the wire format is concerned, enums are int32s. + # Protocol buffers itself also only sends ints; the enum + # objects are simply helper classes for translating names + # and values and it is the user's job to resolve to an int. + # + # Therefore, the non-trivial effort of adding the actual + # enum descriptors seems to add little or no actual value. + # + # FIXME: Eventually, come back and put in the actual enum + # descriptors. + proto_type = ProtoType.INT32 # Set the descriptor. self._descriptor = descriptor_pb2.FieldDescriptorProto( name=self.name, number=self.number, label=3 if self.repeated else 1, - type=self.proto_type, + type=proto_type, type_name=type_name, json_name=self.json_name, proto3_optional=self.optional, diff --git a/proto/message.py b/proto/message.py index 8946ea8f..4873126c 100644 --- a/proto/message.py +++ b/proto/message.py @@ -145,6 +145,7 @@ def __new__(mcls, name, bases, attrs): field_msg = field.message if hasattr(field_msg, "pb") and callable(field_msg.pb): field_msg = field_msg.pb() + # Sanity check: The field's message may not yet be defined if # it was a Message defined in the same file, and the file # descriptor proto has not yet been generated. @@ -153,13 +154,7 @@ def __new__(mcls, name, bases, attrs): # correctly when the file descriptor is created later. if field_msg: proto_imports.add(field_msg.DESCRIPTOR.file.name) - - # Same thing, but for enums. - elif field.enum and not isinstance(field.enum, str): - field_enum = field.enum._meta.pb - - if field_enum: - proto_imports.add(field_enum.file.name) + symbol_database.Default().RegisterMessage(field_msg) # Increment the field index counter. index += 1 @@ -188,13 +183,24 @@ def __new__(mcls, name, bases, attrs): # Determine the filename. # We determine an appropriate proto filename based on the # Python module. - filename = _file_info._FileInfo.proto_file_name( - new_attrs.get("__module__", name.lower()) + filename = "{0}.proto".format( + new_attrs.get("__module__", name.lower()).replace(".", "/") ) # Get or create the information about the file, including the # descriptor to which the new message descriptor shall be added. - file_info = _file_info._FileInfo.maybe_add_descriptor(filename, package) + file_info = _file_info._FileInfo.registry.setdefault( + filename, + _file_info._FileInfo( + descriptor=descriptor_pb2.FileDescriptorProto( + name=filename, package=package, syntax="proto3", + ), + enums=collections.OrderedDict(), + messages=collections.OrderedDict(), + name=filename, + nested={}, + ), + ) # Ensure any imports that would be necessary are assigned to the file # descriptor proto being created. @@ -221,11 +227,6 @@ def __new__(mcls, name, bases, attrs): for child_path in child_paths: desc.nested_type.add().MergeFrom(file_info.nested.pop(child_path)) - # Same thing, but for enums - child_paths = [p for p in file_info.nested_enum.keys() if local_path == p[:-1]] - for child_path in child_paths: - desc.enum_type.add().MergeFrom(file_info.nested_enum.pop(child_path)) - # Add the descriptor to the file if it is a top-level descriptor, # or to a "holding area" for nested messages otherwise. if len(local_path) == 1: @@ -324,24 +325,17 @@ def deserialize(cls, payload: bytes) -> "Message": """ return cls.wrap(cls.pb().FromString(payload)) - def to_json(cls, instance, *, use_integers_for_enums=True) -> str: + def to_json(cls, instance) -> str: """Given a message instance, serialize it to json Args: instance: An instance of this message type, or something compatible (accepted by the type's constructor). - use_integers_for_enums (Optional(bool)): An option that determines whether enum - values should be represented by strings (False) or integers (True). - Default is True. Returns: str: The json string representation of the protocol buffer. """ - return MessageToJson( - cls.pb(instance), - use_integers_for_enums=use_integers_for_enums, - including_default_value_fields=True, - ) + return MessageToJson(cls.pb(instance)) def from_json(cls, payload) -> "Message": """Given a json string representing an instance, diff --git a/tests/clam.py b/tests/clam.py deleted file mode 100644 index a4255fe8..00000000 --- a/tests/clam.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (C) 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import proto - -__protobuf__ = proto.module(package="ocean.clam.v1", manifest={"Clam", "Species",},) - - -class Species(proto.Enum): - UNKNOWN = 0 - SQUAMOSA = 1 - DURASA = 2 - GIGAS = 3 - - -class Clam(proto.Message): - species = proto.Field(proto.ENUM, number=1, enum="Species") - mass_kg = proto.Field(proto.DOUBLE, number=2) diff --git a/tests/mollusc.py b/tests/mollusc.py deleted file mode 100644 index d12e4cb9..00000000 --- a/tests/mollusc.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (C) 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import proto -import zone - -__protobuf__ = proto.module(package="ocean.mollusc.v1", manifest={"Mollusc",},) - - -class Mollusc(proto.Message): - zone = proto.Field(zone.Zone, number=1) diff --git a/tests/test_fields_enum.py b/tests/test_fields_enum.py index a73de131..b494bd0c 100644 --- a/tests/test_fields_enum.py +++ b/tests/test_fields_enum.py @@ -13,19 +13,18 @@ # limitations under the License. import proto -import sys def test_outer_enum_init(): - class Foo(proto.Message): - color = proto.Field(proto.ENUM, number=1, enum="Color") - class Color(proto.Enum): COLOR_UNSPECIFIED = 0 RED = 1 GREEN = 2 BLUE = 3 + class Foo(proto.Message): + color = proto.Field(Color, number=1) + foo = Foo(color=Color.RED) assert foo.color == Color.RED assert foo.color == 1 @@ -35,15 +34,15 @@ class Color(proto.Enum): def test_outer_enum_init_int(): - class Foo(proto.Message): - color = proto.Field(proto.ENUM, number=1, enum="Color") - class Color(proto.Enum): COLOR_UNSPECIFIED = 0 RED = 1 GREEN = 2 BLUE = 3 + class Foo(proto.Message): + color = proto.Field(Color, number=1) + foo = Foo(color=1) assert foo.color == Color.RED assert foo.color == 1 @@ -53,15 +52,15 @@ class Color(proto.Enum): def test_outer_enum_init_str(): - class Foo(proto.Message): - color = proto.Field(proto.ENUM, number=1, enum="Color") - class Color(proto.Enum): COLOR_UNSPECIFIED = 0 RED = 1 GREEN = 2 BLUE = 3 + class Foo(proto.Message): + color = proto.Field(Color, number=1) + foo = Foo(color="RED") assert foo.color == Color.RED assert foo.color == 1 @@ -71,15 +70,15 @@ class Color(proto.Enum): def test_outer_enum_init_dict(): - class Foo(proto.Message): - color = proto.Field(proto.ENUM, number=1, enum="Color") - class Color(proto.Enum): COLOR_UNSPECIFIED = 0 RED = 1 GREEN = 2 BLUE = 3 + class Foo(proto.Message): + color = proto.Field(Color, number=1) + foo = Foo({"color": 1}) assert foo.color == Color.RED assert foo.color == 1 @@ -89,15 +88,15 @@ class Color(proto.Enum): def test_outer_enum_init_dict_str(): - class Foo(proto.Message): - color = proto.Field(proto.ENUM, number=1, enum="Color") - class Color(proto.Enum): COLOR_UNSPECIFIED = 0 RED = 1 GREEN = 2 BLUE = 3 + class Foo(proto.Message): + color = proto.Field(Color, number=1) + foo = Foo({"color": "BLUE"}) assert foo.color == Color.BLUE assert foo.color == 3 @@ -107,15 +106,15 @@ class Color(proto.Enum): def test_outer_enum_init_pb2(): - class Foo(proto.Message): - color = proto.Field(proto.ENUM, number=1, enum="Color") - class Color(proto.Enum): COLOR_UNSPECIFIED = 0 RED = 1 GREEN = 2 BLUE = 3 + class Foo(proto.Message): + color = proto.Field(proto.ENUM, number=1, enum=Color) + foo = Foo(Foo.pb()(color=Color.RED)) assert foo.color == Color.RED assert foo.color == 1 @@ -125,15 +124,15 @@ class Color(proto.Enum): def test_outer_enum_unset(): - class Foo(proto.Message): - color = proto.Field(proto.ENUM, number=1, enum="Color") - class Color(proto.Enum): COLOR_UNSPECIFIED = 0 RED = 1 GREEN = 2 BLUE = 3 + class Foo(proto.Message): + color = proto.Field(proto.ENUM, number=1, enum=Color) + foo = Foo() assert foo.color == Color.COLOR_UNSPECIFIED assert foo.color == 0 @@ -144,15 +143,15 @@ class Color(proto.Enum): def test_outer_enum_write(): - class Foo(proto.Message): - color = proto.Field(proto.ENUM, number=1, enum="Color") - class Color(proto.Enum): COLOR_UNSPECIFIED = 0 RED = 1 GREEN = 2 BLUE = 3 + class Foo(proto.Message): + color = proto.Field(proto.ENUM, number=1, enum=Color) + foo = Foo() foo.color = Color.GREEN assert foo.color == Color.GREEN @@ -162,15 +161,15 @@ class Color(proto.Enum): def test_outer_enum_write_int(): - class Foo(proto.Message): - color = proto.Field(proto.ENUM, number=1, enum="Color") - class Color(proto.Enum): COLOR_UNSPECIFIED = 0 RED = 1 GREEN = 2 BLUE = 3 + class Foo(proto.Message): + color = proto.Field(proto.ENUM, number=1, enum=Color) + foo = Foo() foo.color = 3 assert foo.color == Color.BLUE @@ -181,15 +180,15 @@ class Color(proto.Enum): def test_outer_enum_write_str(): - class Foo(proto.Message): - color = proto.Field(proto.ENUM, number=1, enum="Color") - class Color(proto.Enum): COLOR_UNSPECIFIED = 0 RED = 1 GREEN = 2 BLUE = 3 + class Foo(proto.Message): + color = proto.Field(Color, number=1) + foo = Foo() foo.color = "BLUE" assert foo.color == Color.BLUE @@ -207,7 +206,7 @@ class Color(proto.Enum): GREEN = 2 BLUE = 3 - color = proto.Field(Color, number=1) + color = proto.Field(proto.ENUM, number=1, enum=Color) foo = Foo(color=Foo.Color.RED) assert foo.color == Foo.Color.RED @@ -236,15 +235,15 @@ class Color(proto.Enum): def test_enum_del(): - class Foo(proto.Message): - color = proto.Field(proto.ENUM, number=1, enum="Color") - class Color(proto.Enum): COLOR_UNSPECIFIED = 0 RED = 1 GREEN = 2 BLUE = 3 + class Foo(proto.Message): + color = proto.Field(Color, number=1) + foo = Foo(color=Color.BLUE) del foo.color assert foo.color == Color.COLOR_UNSPECIFIED @@ -253,78 +252,3 @@ class Color(proto.Enum): assert "color" not in foo assert not foo.color assert Foo.pb(foo).color == 0 - - -def test_nested_enum_from_string(): - class Trawl(proto.Message): - # Note: this indirection with the nested field - # is necessary to trigger the exception for testing. - # Setting the field in an existing message accepts strings AND - # checks for valid variants. - # Similarly, constructing a message directly with a top level - # enum field kwarg passed as a string is also handled correctly, i.e. - # s = Squid(zone="ABYSSOPELAGIC") - # does NOT raise an exception. - squids = proto.RepeatedField("Squid", number=1) - - class Squid(proto.Message): - zone = proto.Field(proto.ENUM, number=1, enum="Zone") - - class Zone(proto.Enum): - EPIPELAGIC = 0 - MESOPELAGIC = 1 - BATHYPELAGIC = 2 - ABYSSOPELAGIC = 3 - - t = Trawl(squids=[{"zone": "MESOPELAGIC"}]) - assert t.squids[0] == Squid(zone=Zone.MESOPELAGIC) - - -def test_enum_field_by_string(): - class Squid(proto.Message): - zone = proto.Field(proto.ENUM, number=1, enum="Zone") - - class Zone(proto.Enum): - EPIPELAGIC = 0 - MESOPELAGIC = 1 - BATHYPELAGIC = 2 - ABYSSOPELAGIC = 3 - - s = Squid(zone=Zone.BATHYPELAGIC) - assert s.zone == Zone.BATHYPELAGIC - - -def test_enum_field_by_string_with_package(): - sys.modules[__name__].__protobuf__ = proto.module(package="mollusca.cephalopoda") - try: - - class Octopus(proto.Message): - zone = proto.Field(proto.ENUM, number=1, enum="mollusca.cephalopoda.Zone") - - class Zone(proto.Enum): - EPIPELAGIC = 0 - MESOPELAGIC = 1 - BATHYPELAGIC = 2 - ABYSSOPELAGIC = 3 - - finally: - del sys.modules[__name__].__protobuf__ - - o = Octopus(zone="MESOPELAGIC") - assert o.zone == Zone.MESOPELAGIC - - -def test_enums_in_different_files(): - import mollusc - import zone - - m = mollusc.Mollusc(zone="BATHYPELAGIC") - - assert m.zone == zone.Zone.BATHYPELAGIC - - -def test_enums_in_one_file(): - import clam - - c = clam.Clam(species=clam.Species.DURASA) - assert c.species == clam.Species.DURASA diff --git a/tests/test_file_info_salting.py b/tests/test_file_info_salting.py index c7a91d93..e87731cf 100644 --- a/tests/test_file_info_salting.py +++ b/tests/test_file_info_salting.py @@ -40,7 +40,6 @@ def sample_file_info(name): messages=collections.OrderedDict(), name=filename, nested={}, - nested_enum={}, ), ) diff --git a/tests/test_file_info_salting_with_manifest.py b/tests/test_file_info_salting_with_manifest.py index 1fc50462..0a0e7c57 100644 --- a/tests/test_file_info_salting_with_manifest.py +++ b/tests/test_file_info_salting_with_manifest.py @@ -55,7 +55,6 @@ def sample_file_info(name): messages=collections.OrderedDict(), name=filename, nested={}, - nested_enum={}, ), ) diff --git a/tests/test_json.py b/tests/test_json.py index 58195e89..a300a84b 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -50,48 +50,3 @@ class Squid(proto.Message): s2 = Squid.from_json(json) assert s == s2 - - -def test_json_stringy_enums(): - class Squid(proto.Message): - zone = proto.Field(proto.ENUM, number=1, enum="Zone") - - class Zone(proto.Enum): - EPIPELAGIC = 0 - MESOPELAGIC = 1 - BATHYPELAGIC = 2 - ABYSSOPELAGIC = 3 - - s1 = Squid(zone=Zone.MESOPELAGIC) - json = ( - Squid.to_json(s1, use_integers_for_enums=False) - .replace(" ", "") - .replace("\n", "") - ) - assert json == '{"zone":"MESOPELAGIC"}' - - s2 = Squid.from_json(json) - assert s2.zone == s1.zone - - -def test_json_default_enums(): - class Squid(proto.Message): - zone = proto.Field(proto.ENUM, number=1, enum="Zone") - - class Zone(proto.Enum): - EPIPELAGIC = 0 - MESOPELAGIC = 1 - BATHYPELAGIC = 2 - ABYSSOPELAGIC = 3 - - s = Squid() - assert s.zone == Zone.EPIPELAGIC - json1 = Squid.to_json(s).replace(" ", "").replace("\n", "") - assert json1 == '{"zone":0}' - - json2 = ( - Squid.to_json(s, use_integers_for_enums=False) - .replace(" ", "") - .replace("\n", "") - ) - assert json2 == '{"zone":"EPIPELAGIC"}' diff --git a/tests/test_marshal_types_enum.py b/tests/test_marshal_types_enum.py index 1d302053..129f7016 100644 --- a/tests/test_marshal_types_enum.py +++ b/tests/test_marshal_types_enum.py @@ -18,8 +18,6 @@ import proto from proto.marshal.rules.enums import EnumRule -__protobuf__ = proto.module(package="test.marshal.enum") - def test_to_proto(): class Foo(proto.Enum): diff --git a/tests/zone.py b/tests/zone.py deleted file mode 100644 index 4e7ef402..00000000 --- a/tests/zone.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (C) 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import proto - - -__protobuf__ = proto.module(package="ocean.zone.v1", manifest={"Zone",},) - - -class Zone(proto.Enum): - EPIPELAGIC = 0 - MESOPELAGIC = 1 - BATHYPELAGIC = 2 - ABYSSOPELAGIC = 3