From 6cbfb7e107be26765c32087bee3de913f06a5a1f Mon Sep 17 00:00:00 2001 From: Vincent A Date: Sun, 5 Aug 2018 22:00:19 +0200 Subject: [PATCH] weboob.capabilities: replace stdlib enum with custom enums Python's enums are not extensible, values can't be added and they can't be subclassed. This is problematic if weboob devel adds new values, because stable weboob will not be able to use them. They would need to be replaced by a default value or None, which loses information. Instead, implement custom enums that contain plain constants and support adding values aftewards. --- setup.py | 1 - weboob/applications/flatboob/flatboob.py | 2 +- weboob/capabilities/bank.py | 8 +-- weboob/capabilities/base.py | 75 ++++++++++++++++++------ weboob/capabilities/bugtracker.py | 4 +- weboob/capabilities/calendar.py | 10 ++-- weboob/capabilities/file.py | 6 +- weboob/capabilities/gallery.py | 4 +- weboob/capabilities/housing.py | 12 ++-- weboob/capabilities/parcel.py | 4 +- 10 files changed, 82 insertions(+), 44 deletions(-) diff --git a/setup.py b/setup.py index 37f180acde..a028536583 100755 --- a/setup.py +++ b/setup.py @@ -138,7 +138,6 @@ def install_weboob(): 'Pillow', 'mechanize; python_version < "3.0"', 'futures; python_version < "3.2"', - 'enum34; python_version < "3.4"', ] if not options.deps: diff --git a/weboob/applications/flatboob/flatboob.py b/weboob/applications/flatboob/flatboob.py index ad02687e15..4e9635ac08 100644 --- a/weboob/applications/flatboob/flatboob.py +++ b/weboob/applications/flatboob/flatboob.py @@ -174,7 +174,7 @@ def do_search(self, line): query.house_types.append(value) _type = None - posts_types = sorted(POSTS_TYPES, key=lambda e: e.value) + posts_types = sorted(POSTS_TYPES) while _type not in range(len(posts_types)): for i, t in enumerate(posts_types): print(' %s%2d)%s %s' % (self.BOLD, diff --git a/weboob/capabilities/bank.py b/weboob/capabilities/bank.py index d3e9a91daf..16e7bc2473 100644 --- a/weboob/capabilities/bank.py +++ b/weboob/capabilities/bank.py @@ -29,7 +29,7 @@ from weboob.tools.compat import unicode from .base import BaseObject, Field, StringField, DecimalField, IntField, \ - UserError, Currency, NotAvailable, EnumField, IntEnum + UserError, Currency, NotAvailable, EnumField, Enum from .date import DateField from .collection import CapCollection @@ -190,7 +190,7 @@ class Recipient(BaseAccount): iban = StringField('International Bank Account Number') -class AccountType(IntEnum): +class AccountType(Enum): UNKNOWN = 0 CHECKING = 1 "Transaction, everyday transactions" @@ -312,7 +312,7 @@ class Loan(Account): next_payment_date = DateField('Date of the next payment') -class TransactionType(IntEnum): +class TransactionType(Enum): UNKNOWN = 0 TRANSFER = 1 ORDER = 2 @@ -436,7 +436,7 @@ def __repr__(self): return '' % (self.label, self.code, self.valuation) -class PocketCondition(IntEnum): +class PocketCondition(Enum): UNKNOWN = 0 DATE = 1 AVAILABLE = 2 diff --git a/weboob/capabilities/base.py b/weboob/capabilities/base.py index 5e84851f35..369f76f186 100644 --- a/weboob/capabilities/base.py +++ b/weboob/capabilities/base.py @@ -18,7 +18,6 @@ # along with weboob. If not, see . from collections import OrderedDict -from enum import Enum as _Enum import warnings import re from decimal import Decimal @@ -32,27 +31,65 @@ __all__ = ['UserError', 'FieldNotFound', 'NotAvailable', 'FetchError', 'NotLoaded', 'Capability', 'Field', 'IntField', 'DecimalField', 'FloatField', 'StringField', 'BytesField', 'BoolField', - 'Enum', 'IntEnum', 'StrEnum', 'EnumField', + 'Enum', 'EnumField', 'empty', 'BaseObject'] -class Enum(_Enum): - pass +class EnumMeta(type): + @classmethod + def __prepare__(mcs, name, bases, **kwargs): + # in python3.6, default namespace keeps declaration order + # in python>=3 but <3.6, force ordered namespace + # doesn't work in python2 + return OrderedDict() + + def __init__(cls, name, bases, attrs, *args, **kwargs): + super(EnumMeta, cls).__init__(name, bases, attrs, *args, **kwargs) + attrs = [(k, v) for k, v in attrs.items() if not callable(v) and not k.startswith('__')] + if sys.version_info.major < 3: + # can't have original declaration order, at least sort by value + attrs.sort(key=lambda kv: kv[1]) + cls.__members__ = OrderedDict(attrs) + + def __setattr__(cls, name, value): + super(EnumMeta, cls).__setattr__(name, value) + if not callable(value) and not name.startswith('__'): + cls.__members__[name] = value + + def __call__(cls, *args, **kwargs): + raise ValueError("Enum type can't be instanciated") + + @property + def _items(cls): + return cls.__members__.items() + + @property + def _keys(cls): + return cls.__members__.keys() + + @property + def _values(cls): + return cls.__members__.values() + + @property + def _types(cls): + return set(map(type, cls._values)) + def __iter__(cls): + return iter(cls.__members__.values()) -class StrEnum(unicode, Enum): - if sys.version_info.major < 3: - # cannot use StrConv helper, else for some reason it will error like: - # TypeError: cannot be pickled - def __str__(self): - return unicode(self.value).encode('utf-8') - else: - def __str__(self): - return str(self.value) + def __len__(cls): + return len(cls.__members__) + def __contains__(cls, value): + return value in cls.__members__.values() -class IntEnum(int, Enum): - __str__ = int.__str__ + def __getitem__(cls, k): + return cls.__members__[k] + + +class Enum(with_metaclass(EnumMeta, object)): + pass def empty(value): @@ -306,13 +343,15 @@ def convert(self, value): class EnumField(Field): def __init__(self, doc, enum, **kwargs): - super(EnumField, self).__init__(doc, enum, **kwargs) - if not issubclass(enum, _Enum): + if not issubclass(enum, Enum): raise TypeError('invalid enum type: %r' % enum) + super(EnumField, self).__init__(doc, *enum._types, **kwargs) self.enum = enum def convert(self, value): - return self.enum(value) + if value not in self.enum._values: + raise ValueError('value %r does not belong to enum %s' % (value, self.enum)) + return value class _BaseObjectMeta(type): diff --git a/weboob/capabilities/bugtracker.py b/weboob/capabilities/bugtracker.py index 9f43e1390d..c846f259cf 100644 --- a/weboob/capabilities/bugtracker.py +++ b/weboob/capabilities/bugtracker.py @@ -20,7 +20,7 @@ from weboob.tools.compat import unicode from .base import Capability, BaseObject, Field, StringField,\ - IntField, UserError, IntEnum + IntField, UserError, Enum from .date import DateField, DeltaField @@ -135,7 +135,7 @@ def __repr__(self): return '' % self.name -class StatusType(IntEnum): +class StatusType(Enum): NEW = 0 PROGRESS = 1 RESOLVED = 2 diff --git a/weboob/capabilities/calendar.py b/weboob/capabilities/calendar.py index 59d2576177..a5cb44ab88 100644 --- a/weboob/capabilities/calendar.py +++ b/weboob/capabilities/calendar.py @@ -19,7 +19,7 @@ from .base import ( BaseObject, StringField, IntField, FloatField, Field, EnumField, - StrEnum, + Enum, ) from .collection import CapCollection, CollectionNotFound, Collection from .date import DateField @@ -30,7 +30,7 @@ __all__ = ['BaseCalendarEvent', 'CapCalendarEvent'] -class CATEGORIES(StrEnum): +class CATEGORIES(Enum): CONCERT = u'Concert' CINE = u'Cinema' THEATRE = u'Theatre' @@ -45,18 +45,18 @@ class CATEGORIES(StrEnum): #the following elements deal with ICalendar stantdards #see http://fr.wikipedia.org/wiki/ICalendar#Ev.C3.A9nements_.28VEVENT.29 -class TRANSP(StrEnum): +class TRANSP(Enum): OPAQUE = u'OPAQUE' TRANSPARENT = u'TRANSPARENT' -class STATUS(StrEnum): +class STATUS(Enum): TENTATIVE = u'TENTATIVE' CONFIRMED = u'CONFIRMED' CANCELLED = u'CANCELLED' -class TICKET(StrEnum): +class TICKET(Enum): AVAILABLE = u'Available' NOTAVAILABLE = u'Not available' CLOSED = u'Closed' diff --git a/weboob/capabilities/file.py b/weboob/capabilities/file.py index 0f4d519ac4..1822bae3e8 100644 --- a/weboob/capabilities/file.py +++ b/weboob/capabilities/file.py @@ -19,14 +19,14 @@ from weboob.tools.compat import long -from .base import Capability, BaseObject, NotAvailable, Field, StringField, IntField, StrEnum, IntEnum +from .base import Capability, BaseObject, NotAvailable, Field, StringField, IntField, Enum from .date import DateField __all__ = ['BaseFile', 'CapFile'] -class LICENSES(StrEnum): +class LICENSES(Enum): OTHER = u'Other license' PD = u'Public Domain' COPYRIGHT = u'All rights reserved' @@ -74,7 +74,7 @@ def page_url(self): return self.id2url(self.id) -class SearchSort(IntEnum): +class SearchSort(Enum): RELEVANCE = 0 RATING = 1 VIEWS = 2 diff --git a/weboob/capabilities/gallery.py b/weboob/capabilities/gallery.py index f0b7be5394..ff05740347 100644 --- a/weboob/capabilities/gallery.py +++ b/weboob/capabilities/gallery.py @@ -21,7 +21,7 @@ from weboob.capabilities.image import BaseImage as CIBaseImage, Thumbnail from weboob.tools.compat import unicode from .base import Capability, BaseObject, NotLoaded, Field, StringField, \ - IntField, FloatField, IntEnum + IntField, FloatField, Enum from .date import DateField @@ -98,7 +98,7 @@ def __iscomplete__(self): return self.data is not NotLoaded -class SearchSort(IntEnum): +class SearchSort(Enum): RELEVANCE = 0 RATING = 1 VIEWS = 2 diff --git a/weboob/capabilities/housing.py b/weboob/capabilities/housing.py index 364faf4307..60ad763950 100644 --- a/weboob/capabilities/housing.py +++ b/weboob/capabilities/housing.py @@ -19,7 +19,7 @@ from .base import Capability, BaseObject, Field, IntField, DecimalField, \ - StringField, BytesField, StrEnum, EnumField, UserError + StringField, BytesField, Enum, EnumField, UserError from .date import DateField __all__ = [ @@ -58,13 +58,13 @@ def __repr__(self): return '' % (self.id, len(self.data) if self.data else 0, self.url) -class UTILITIES(StrEnum): +class UTILITIES(Enum): INCLUDED = u'C.C.' EXCLUDED = u'H.C.' UNKNOWN = u'' -class ENERGY_CLASS(StrEnum): +class ENERGY_CLASS(Enum): A = u'A' B = u'B' C = u'C' @@ -74,7 +74,7 @@ class ENERGY_CLASS(StrEnum): G = u'G' -class POSTS_TYPES(StrEnum): +class POSTS_TYPES(Enum): RENT=u'RENT' SALE = u'SALE' SHARING = u'SHARING' @@ -82,12 +82,12 @@ class POSTS_TYPES(StrEnum): VIAGER = u'VIAGER' -class ADVERT_TYPES(StrEnum): +class ADVERT_TYPES(Enum): PROFESSIONAL = u'Professional' PERSONAL = u'Personal' -class HOUSE_TYPES(StrEnum): +class HOUSE_TYPES(Enum): APART = u'Apartment' HOUSE = u'House' PARKING = u'Parking' diff --git a/weboob/capabilities/parcel.py b/weboob/capabilities/parcel.py index e8a8863c28..319ef5751d 100644 --- a/weboob/capabilities/parcel.py +++ b/weboob/capabilities/parcel.py @@ -18,7 +18,7 @@ # along with weboob. If not, see . -from .base import Capability, BaseObject, Field, StringField, UserError, IntEnum +from .base import Capability, BaseObject, Field, StringField, UserError, Enum from .date import DateField @@ -31,7 +31,7 @@ def __repr__(self): return '' % (self.date, self.activity, self.location) -class ParcelState(IntEnum): +class ParcelState(Enum): UNKNOWN = 0 PLANNED = 1 IN_TRANSIT = 2 -- GitLab