:EP: 4
:Title: Updating specification
:Status: Accepted

******************************
EP004 - Updating specification
******************************

Note: all the (draft) code is available at https://transfer.sh/ac8jc/version-inheritance.tar.gz.

Description
###########

Suppose there are only two versions: ``v1`` and ``2`` in the package ``example``.
The code in this section follows an approach similar to what we tried to do before our current approach. It didn't work well because of the problems to be presented.

.. code:: bash

  .
  ├── example
  │   ├── __init__.py
  │   ├── v1
  │   │   ├── __init__.py
  │   │   ├── myclass.py
  │   │   └── myenum.py
  │   └── v2
  │       ├── __init__.py
  │       ├── myclass.py
  │       └── myenum.py
  ├── v1.py
  └── v2.py

Different Enums
---------------
In the ``v2`` specification, a value was changed and a name, added.

File ``example/v1/myenum.py``

.. code:: python

  from enum import Enum

  class MyEnum(Enum):
      ENUM_01 = 1

File ``example/v2/myenum.py``

.. code:: python

  from enum import Enum

  class MyEnum(Enum):
      ENUM_01 = 10
      ENUM_02 = 20


Different class attributes
--------------------------
``attr_0`` was removed and ``attr_z``, renamed.

File ``example/v1/myclass.py``

.. code:: python

  from pyof.foundation.base import GenericStruct
  from pyof.foundation.basic_types import UBInt8
  from example.v1.myenum import MyEnum

  class MyClass(GenericStruct):
      attr_0 = UBInt8(enum_ref=MyEnum)
      attr_a = UBInt8(enum_ref=MyEnum)
      attr_z = UBInt8(enum_ref=MyEnum)
      attr_c = UBInt8(enum_ref=MyEnum)

      def __init__(self, attr_a=None):
          if attr_a is None:
              self.attr_a = MyEnum.ENUM_01


File ``example/v2/myclass.py``

.. code:: python

  from pyof.foundation.basic_types import UBInt8
  from example.v1.myclass import MyClass as MyClassV1
  from example.v2.myenum import MyEnum

  class MyClass(MyClassV1):
      attr_a = UBInt8(enum_ref=MyEnum)
      attr_b = UBInt8(enum_ref=MyEnum)
      attr_c = UBInt8(enum_ref=MyEnum)


Problems found
--------------

Let's run a simple program to print the instance attributes.
The only relevant difference is in the second line:

File ``v1.py``

.. code:: python

  import pprint
  from example.v1.myclass import MyClass

  pprint.pprint(MyClass().__dict__)


File ``v2.py``

.. code:: python

  import pprint
  from example.v2.myclass import MyClass

  pprint.pprint(MyClass().__dict__)
  print('ENUM_01 is 10 in v2, not 1.')


Output of ``v1.py`` (run in the same folder)

.. code:: bash

  {'attr_a': <MyEnum.ENUM_01: 1>}

Output of ``v2.py`` (run in the same folder)

.. code:: bash

  {'attr_a': <MyEnum.ENUM_01: 1>}
  ENUM_01 is 10 in v2, not 1.

We can notice the following problems:

1. ``v2/myclass.py`` imports ``MyEnum`` from ``v2.myenum`` but ``v1.myenum`` is used instead. This happens because the code in ``v1/myclass.py`` (parent class) uses ``v1.myenum`` in its constructor;
2. Some class attributes were rewritten because the child class ``__ordered__`` is empty. The worst case is when no class attributes are changed (== ctrl+c, ctrl+v == difficult to maintain).

Other problems that don't happen in the example:

1. If a class is not changed at all, we must create a file in ``v2`` so the user can import from ``example.v2``.
2. `Issue 157 <https://github.com/kytos/python-openflow/issues/157>`_: currently, OF version is hard coded, so it will be ``v0x01`` when extended by ``v0x01`` (and not overridden)

Solution
########

This may not be the best approach, but certainly will stimulate better solutions.

Enum manipulation
-----------------

This is more like a tip than a "solution":

File ``v2/myenum.py``

.. code:: python

  from enum import Enum
  # Explicitly import from v1 to update its attributes
  from example.v1.myenum import MyEnum as MyEnumV1

  def update_version(old_enum):
      """Example on how to update an Enum."""
      items = {item.name: item.value for item in old_enum}
      # Updating ENUM_01's value
      items['ENUM_01'] = 10
      # Adding ENUM_02
      items['ENUM_02'] = 20
      return Enum('MyEnum', items)

  MyEnum = update_version(MyEnumV1)

Class attributes (Problem 2)
----------------------------

This approach is simple and requires only a small change in the ``MetaStruct`` class:

File ``v2/myclass.py``

.. code:: python

  from collections import OrderedDict
  from pyof.foundation.basic_types import UBInt8
  # Explicitly import from v1 to update its attributes
  from example.v1.myclass import MyClass as MyClassV1

  def update_version(old_cls):
      """Examples on how to manipulate an OrderedDict."""
      # Take care not to change MyClassV1.__ordered__ by making a copy
      ordered = old_cls.__ordered__.copy()

      # Removing attr_0
      ordered.pop('attr_0')

      # Renaming attr_z to attr_b maintaining the order and its value
      ## From OrderedDict to list of type [(attr name, attr value), ...]
      items = list(ordered.items())
      ## attr_z is the second attribute
      z_value = items[1][1]
      ## Changing only attribute's name
      items[1] = ('attr_b', z_value)

      return OrderedDict(items)


  class MyClass(MyClassV1):
      # One cannot use a method here because this class is not defined yet.
      __ordered__ = update_version(MyClassV1)

Changes in the ``MetaStruct`` class of ``pyof/foundation/base.py``. Basically, we add one if clause.

.. code:: diff

  diff --git a/pyof/foundation/base.py b/pyof/foundation/base.py
  index af23dbe..601c94a 100644
  --- a/pyof/foundation/base.py
  +++ b/pyof/foundation/base.py
  @@ -247,10 +247,11 @@ class MetaStruct(type):
       def __new__(mcs, name, bases, classdict):
           """Add ``__ordered__`` attribute with attributes in declared order."""
           # Skip methods and private attributes
  -        classdict['__ordered__'] = OrderedDict([(key, type(value)) for
  -                                                key, value in classdict.items()
  -                                                if key[0] != '_' and not
  -                                                hasattr(value, '__call__')])
  +        if '__ordered__' not in classdict:
  +            classdict['__ordered__'] = OrderedDict([(k, type(v)) for
  +                                                    k, v in classdict.items()
  +                                                    if k[0] != '_' and not
  +                                                    hasattr(v, '__call__')])
           return type.__new__(mcs, name, bases, classdict)


Parent's Enum (Problem 1)
-------------------------

This is discussed in :doc:`EP005`
