Skip to content

Commit

Permalink
Add separate type repository for each child of ODATAVersion
Browse files Browse the repository at this point in the history
In every release of OData new types are not only added and removed but
also there are changes to the existing types notably in formatting.
Thus, there is a need to have separate type repository for each OData
version.

Let's see an example of Edm.Double JSON format:
OData V2: 3.141d
OData V4: 3.141

https://www.odata.org/documentation/odata-version-2-0/overview/#AbstractTypeSystem

http://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html
  • Loading branch information
mamiksik committed Nov 8, 2019
1 parent 3515a36 commit c93cccd
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 78 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Client can be created from local metadata - Jakub Filak
- support all standard EDM schema versions - Jakub Filak
- Splits python representation of metadata and metadata parsing - Martin Miksik
- Separate type repositories for individual versions of OData - Martin Miksik

### Fixed
- make sure configured error policies are applied for Annotations referencing
Expand Down
16 changes: 9 additions & 7 deletions pyodata/config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
""" Contains definition of configuration class for PyOData and for ODATA versions. """

from abc import ABC, abstractmethod
from typing import Type, List, Dict, Callable
from typing import Type, List, Dict, Callable, TYPE_CHECKING

from pyodata.policies import PolicyFatal, ParserError, ErrorPolicy

# pylint: disable=cyclic-import
if TYPE_CHECKING:
from pyodata.model.elements import Typ # noqa


class ODATAVersion(ABC):
""" This is base class for different OData releases. In it we define what are supported types, elements and so on.
Expand All @@ -15,21 +19,19 @@ def __init__(self):
raise RuntimeError('ODATAVersion and its children are intentionally stateless, '
'therefore you can not create instance of them')

# Separate dictionary of all registered types (primitive, complex and collection variants) for each child
Types: Dict[str, 'Typ'] = dict()

@staticmethod
@abstractmethod
def supported_primitive_types() -> List[str]:
def primitive_types() -> List['Typ']:
""" Here we define which primitive types are supported and what is their python representation"""

@staticmethod
@abstractmethod
def from_etree_callbacks() -> Dict[object, Callable]:
""" Here we define which elements are supported and what is their python representation"""

@classmethod
def is_primitive_type_supported(cls, type_name):
""" Convenience method which decides whatever given type is supported."""
return type_name in cls.supported_primitive_types()


class Config:
# pylint: disable=too-many-instance-attributes,missing-docstring
Expand Down
68 changes: 18 additions & 50 deletions pyodata/model/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
from pyodata.config import Config
from pyodata.exceptions import PyODataModelError, PyODataException, PyODataParserError

from pyodata.model.type_traits import EdmBooleanTypTraits, EdmDateTimeTypTraits, EdmPrefixedTypTraits, \
EdmIntTypTraits, EdmLongIntTypTraits, EdmStringTypTraits, TypTraits, EdmStructTypTraits, EnumTypTrait
from pyodata.model.type_traits import TypTraits, EdmStructTypTraits, EnumTypTrait


IdentifierInfo = collections.namedtuple('IdentifierInfo', 'namespace name')
Expand Down Expand Up @@ -80,69 +79,39 @@ def parse(value):


class Types:
"""Repository of all available OData types
""" Repository of all available OData types in given version
Since each type has instance of appropriate type, this
repository acts as central storage for all instances. The
rule is: don't create any type instances if not necessary,
always reuse existing instances if possible
"""

# dictionary of all registered types (primitive, complex and collection variants)
Types = None

@staticmethod
def _build_types():
"""Create and register instances of all primitive Edm types"""

if Types.Types is None:
Types.Types = {}

Types.register_type(Typ('Null', 'null'))
Types.register_type(Typ('Edm.Binary', 'binary\'\''))
Types.register_type(Typ('Edm.Boolean', 'false', EdmBooleanTypTraits()))
Types.register_type(Typ('Edm.Byte', '0'))
Types.register_type(Typ('Edm.DateTime', 'datetime\'2000-01-01T00:00\'', EdmDateTimeTypTraits()))
Types.register_type(Typ('Edm.Decimal', '0.0M'))
Types.register_type(Typ('Edm.Double', '0.0d'))
Types.register_type(Typ('Edm.Single', '0.0f'))
Types.register_type(
Typ('Edm.Guid', 'guid\'00000000-0000-0000-0000-000000000000\'', EdmPrefixedTypTraits('guid')))
Types.register_type(Typ('Edm.Int16', '0', EdmIntTypTraits()))
Types.register_type(Typ('Edm.Int32', '0', EdmIntTypTraits()))
Types.register_type(Typ('Edm.Int64', '0L', EdmLongIntTypTraits()))
Types.register_type(Typ('Edm.SByte', '0'))
Types.register_type(Typ('Edm.String', '\'\'', EdmStringTypTraits()))
Types.register_type(Typ('Edm.Time', 'time\'PT00H00M\''))
Types.register_type(Typ('Edm.DateTimeOffset', 'datetimeoffset\'0000-00-00T00:00:00\''))
def register_type(typ: 'Typ', config: Config):
"""Add new type to the ODATA version type repository as well as its collection variant"""

@staticmethod
def register_type(typ):
"""Add new type to the type repository as well as its collection variant"""

# build types hierarchy on first use (lazy creation)
if Types.Types is None:
Types._build_types()
o_version = config.odata_version

# register type only if it doesn't exist
# pylint: disable=unsupported-membership-test
if typ.name not in Types.Types:
# pylint: disable=unsupported-assignment-operation
Types.Types[typ.name] = typ
if typ.name not in o_version.Types:
o_version.Types[typ.name] = typ

# automatically create and register collection variant if not exists
collection_name = 'Collection({})'.format(typ.name)
# pylint: disable=unsupported-membership-test
if collection_name not in Types.Types:
if collection_name not in o_version.Types:
collection_typ = Collection(typ.name, typ)
# pylint: disable=unsupported-assignment-operation
Types.Types[collection_name] = collection_typ
o_version.Types[collection_name] = collection_typ

@staticmethod
def from_name(name, config: Config):
o_version = config.odata_version

# build types hierarchy on first use (lazy creation)
if Types.Types is None:
Types._build_types()
if not o_version.Types:
o_version.Types = dict()
for typ in o_version.primitive_types():
Types.register_type(typ, config)

search_name = name

Expand All @@ -152,12 +121,11 @@ def from_name(name, config: Config):
name = name[11:-1] # strip collection() decorator
search_name = 'Collection({})'.format(name)

if not config.odata_version.is_primitive_type_supported(name):
try:
return o_version.Types[search_name]
except KeyError:
raise KeyError('Requested primitive type is not supported in this version of ODATA')

# pylint: disable=unsubscriptable-object
return Types.Types[search_name]

@staticmethod
def parse_type_name(type_name):

Expand Down
36 changes: 19 additions & 17 deletions pyodata/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import logging
from typing import List

from pyodata.model.type_traits import EdmBooleanTypTraits, EdmDateTimeTypTraits, EdmPrefixedTypTraits, \
EdmIntTypTraits, EdmLongIntTypTraits, EdmStringTypTraits
from pyodata.policies import ParserError
from pyodata.config import ODATAVersion, Config
from pyodata.exceptions import PyODataParserError, PyODataModelError
Expand Down Expand Up @@ -52,24 +54,24 @@ def from_etree_callbacks():
}

@staticmethod
def supported_primitive_types() -> List[str]:
def primitive_types() -> List[Typ]:
return [
'Null',
'Edm.Binary',
'Edm.Boolean',
'Edm.Byte',
'Edm.DateTime',
'Edm.Decimal',
'Edm.Double',
'Edm.Single',
'Edm.Guid',
'Edm.Int16',
'Edm.Int32',
'Edm.Int64',
'Edm.SByte',
'Edm.String',
'Edm.Time',
'Edm.DateTimeOffset',
Typ('Null', 'null'),
Typ('Edm.Binary', 'binary\'\''),
Typ('Edm.Boolean', 'false', EdmBooleanTypTraits()),
Typ('Edm.Byte', '0'),
Typ('Edm.DateTime', 'datetime\'2000-01-01T00:00\'', EdmDateTimeTypTraits()),
Typ('Edm.Decimal', '0.0M'),
Typ('Edm.Double', '0.0d'),
Typ('Edm.Single', '0.0f'),
Typ('Edm.Guid', 'guid\'00000000-0000-0000-0000-000000000000\'', EdmPrefixedTypTraits('guid')),
Typ('Edm.Int16', '0', EdmIntTypTraits()),
Typ('Edm.Int32', '0', EdmIntTypTraits()),
Typ('Edm.Int64', '0L', EdmLongIntTypTraits()),
Typ('Edm.SByte', '0'),
Typ('Edm.String', '\'\'', EdmStringTypTraits()),
Typ('Edm.Time', 'time\'PT00H00M\''),
Typ('Edm.DateTimeOffset', 'datetimeoffset\'0000-00-00T00:00:00\'')
]

# pylint: disable=too-many-locals,too-many-branches,too-many-statements, protected-access,missing-docstring
Expand Down
31 changes: 27 additions & 4 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from pyodata.config import Config, ODATAVersion
from pyodata.exceptions import PyODataParserError
from pyodata.model.builder import MetadataBuilder
from pyodata.model.elements import Schema, Types
from pyodata.model.elements import Schema, Types, Typ
from v2 import ODataV2


def test_from_etree_mixin(metadata):
Expand All @@ -29,9 +30,9 @@ def test_supported_primitive_types():

class EmptyODATA(ODATAVersion):
@staticmethod
def supported_primitive_types() -> List[str]:
def primitive_types() -> List[Typ]:
return [
'Edm.Binary'
Typ('Edm.Binary', 'binary\'\'')
]

config = Config(EmptyODATA)
Expand All @@ -51,11 +52,33 @@ def from_etree_callbacks():
return {}

@staticmethod
def supported_primitive_types() -> List[str]:
def primitive_types() -> List[Typ]:
return []

with pytest.raises(RuntimeError) as typ_ex_info:
EmptyODATA()

assert typ_ex_info.value.args[0] == 'ODATAVersion and its children are intentionally stateless, ' \
'therefore you can not create instance of them'


def test_types_repository_separation():

class TestODATA(ODATAVersion):
@staticmethod
def primitive_types() -> List['Typ']:
return [
Typ('PrimitiveType', '0')
]

config_test = Config(TestODATA)
config_v2 = Config(ODataV2)

assert TestODATA.Types is None
assert TestODATA.Types == ODataV2.Types

# Build type repository by initial call
Types.from_name('PrimitiveType', config_test)
Types.from_name('Edm.Int16', config_v2)

assert TestODATA.Types != ODataV2.Types

0 comments on commit c93cccd

Please sign in to comment.