From 19709ff10e1a92404323f494b48ce398b2533950 Mon Sep 17 00:00:00 2001 From: Sam Pegler Date: Thu, 9 Jan 2020 21:54:15 +0000 Subject: [PATCH 1/4] Partial: Remove encodings and future imports. --- flask_restplus/__about__.py | 1 - flask_restplus/__init__.py | 3 --- flask_restplus/_http.py | 1 - flask_restplus/api.py | 3 --- flask_restplus/apidoc.py | 3 --- flask_restplus/cors.py | 3 --- flask_restplus/errors.py | 3 --- flask_restplus/fields.py | 3 --- flask_restplus/inputs.py | 1 - flask_restplus/marshalling.py | 3 --- flask_restplus/mask.py | 3 --- flask_restplus/model.py | 3 --- flask_restplus/namespace.py | 3 --- flask_restplus/postman.py | 3 --- flask_restplus/representations.py | 3 --- flask_restplus/reqparse.py | 3 --- flask_restplus/resource.py | 3 --- flask_restplus/swagger.py | 3 --- flask_restplus/utils.py | 3 --- tests/conftest.py | 3 --- tests/test_accept.py | 3 --- tests/test_api.py | 3 --- tests/test_apidoc.py | 3 --- tests/test_cors.py | 3 --- tests/test_errors.py | 3 --- tests/test_fields.py | 3 --- tests/test_fields_mask.py | 3 --- tests/test_inputs.py | 3 --- tests/test_marshalling.py | 3 --- tests/test_model.py | 3 --- tests/test_namespace.py | 3 --- tests/test_payload.py | 3 --- tests/test_postman.py | 3 --- tests/test_reqparse.py | 3 --- tests/test_schemas.py | 3 --- tests/test_swagger.py | 3 --- tests/test_swagger_utils.py | 3 --- tests/test_utils.py | 3 --- 38 files changed, 108 deletions(-) diff --git a/flask_restplus/__about__.py b/flask_restplus/__about__.py index 1b6194b0..7ab99eaf 100644 --- a/flask_restplus/__about__.py +++ b/flask_restplus/__about__.py @@ -1,3 +1,2 @@ -# -*- coding: utf-8 -*- __version__ = '0.13.1.dev' __description__ = 'Fully featured framework for fast, easy and documented API development with Flask' diff --git a/flask_restplus/__init__.py b/flask_restplus/__init__.py index 4929bb1a..12941614 100644 --- a/flask_restplus/__init__.py +++ b/flask_restplus/__init__.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - from . import fields, reqparse, apidoc, inputs, cors from .api import Api # noqa from .marshalling import marshal, marshal_with, marshal_with_field # noqa diff --git a/flask_restplus/_http.py b/flask_restplus/_http.py index b86714c1..8fee4765 100644 --- a/flask_restplus/_http.py +++ b/flask_restplus/_http.py @@ -1,4 +1,3 @@ -# encoding: utf-8 """ This file is backported from Python 3.5 http built-in module. """ diff --git a/flask_restplus/api.py b/flask_restplus/api.py index b5433ce3..9ff93ae5 100644 --- a/flask_restplus/api.py +++ b/flask_restplus/api.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import difflib import inspect from itertools import chain diff --git a/flask_restplus/apidoc.py b/flask_restplus/apidoc.py index a6f7a342..a2e43d4d 100644 --- a/flask_restplus/apidoc.py +++ b/flask_restplus/apidoc.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from flask import url_for, Blueprint, render_template diff --git a/flask_restplus/cors.py b/flask_restplus/cors.py index d2d0a2e8..523a014e 100644 --- a/flask_restplus/cors.py +++ b/flask_restplus/cors.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from datetime import timedelta from flask import make_response, request, current_app from functools import update_wrapper diff --git a/flask_restplus/errors.py b/flask_restplus/errors.py index 24de3d51..8759c93e 100644 --- a/flask_restplus/errors.py +++ b/flask_restplus/errors.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import flask from werkzeug.exceptions import HTTPException diff --git a/flask_restplus/fields.py b/flask_restplus/fields.py index 6b7fdc36..3041be17 100644 --- a/flask_restplus/fields.py +++ b/flask_restplus/fields.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import re import fnmatch import inspect diff --git a/flask_restplus/inputs.py b/flask_restplus/inputs.py index 5a9fa6ca..d236b3de 100644 --- a/flask_restplus/inputs.py +++ b/flask_restplus/inputs.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- ''' This module provide some helpers for advanced types parsing. diff --git a/flask_restplus/marshalling.py b/flask_restplus/marshalling.py index 84c8feb2..2cde2b34 100644 --- a/flask_restplus/marshalling.py +++ b/flask_restplus/marshalling.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - try: from collections.abc import OrderedDict except ImportError: diff --git a/flask_restplus/mask.py b/flask_restplus/mask.py index ee61669d..5d3a5cad 100644 --- a/flask_restplus/mask.py +++ b/flask_restplus/mask.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import - import logging import re import six diff --git a/flask_restplus/model.py b/flask_restplus/model.py index cbe1a3cf..993377c3 100644 --- a/flask_restplus/model.py +++ b/flask_restplus/model.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import copy import re import warnings diff --git a/flask_restplus/namespace.py b/flask_restplus/namespace.py index 9c1b7b57..1c9fc52d 100644 --- a/flask_restplus/namespace.py +++ b/flask_restplus/namespace.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import inspect import warnings import logging diff --git a/flask_restplus/postman.py b/flask_restplus/postman.py index 0af8ee2d..38d48245 100644 --- a/flask_restplus/postman.py +++ b/flask_restplus/postman.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import - from time import time from uuid import uuid5, NAMESPACE_URL diff --git a/flask_restplus/representations.py b/flask_restplus/representations.py index a66d7eaa..1831b3f8 100644 --- a/flask_restplus/representations.py +++ b/flask_restplus/representations.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import - try: from ujson import dumps except ImportError: diff --git a/flask_restplus/reqparse.py b/flask_restplus/reqparse.py index d24b4d38..fed31ac6 100644 --- a/flask_restplus/reqparse.py +++ b/flask_restplus/reqparse.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import decimal import six diff --git a/flask_restplus/resource.py b/flask_restplus/resource.py index 9bdd2b11..f6549caa 100644 --- a/flask_restplus/resource.py +++ b/flask_restplus/resource.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from flask import request from flask.views import MethodView from werkzeug.wrappers import BaseResponse diff --git a/flask_restplus/swagger.py b/flask_restplus/swagger.py index 7c534b26..43c3a391 100644 --- a/flask_restplus/swagger.py +++ b/flask_restplus/swagger.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import - import itertools import re diff --git a/flask_restplus/utils.py b/flask_restplus/utils.py index a094d2dd..b408b719 100644 --- a/flask_restplus/utils.py +++ b/flask_restplus/utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import re try: diff --git a/tests/conftest.py b/tests/conftest.py index f8c23e71..e242e64e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import json import pytest diff --git a/tests/test_accept.py b/tests/test_accept.py index fb6f5ee5..12002bce 100644 --- a/tests/test_accept.py +++ b/tests/test_accept.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import flask_restplus as restplus diff --git a/tests/test_api.py b/tests/test_api.py index 21d02471..baedf24b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import copy from flask import url_for, Blueprint diff --git a/tests/test_apidoc.py b/tests/test_apidoc.py index 1f0a9bca..870ec71e 100644 --- a/tests/test_apidoc.py +++ b/tests/test_apidoc.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import pytest from flask import url_for, Blueprint diff --git a/tests/test_cors.py b/tests/test_cors.py index fbf79093..da301d79 100644 --- a/tests/test_cors.py +++ b/tests/test_cors.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from flask_restplus import Api, Resource, cors diff --git a/tests/test_errors.py b/tests/test_errors.py index b196efe3..af327fe0 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import json import logging diff --git a/tests/test_fields.py b/tests/test_fields.py index 837e607d..bc2ca2c8 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - try: from collections.abc import OrderedDict except ImportError: diff --git a/tests/test_fields_mask.py b/tests/test_fields_mask.py index 7ffeefc8..1af8c930 100644 --- a/tests/test_fields_mask.py +++ b/tests/test_fields_mask.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import json import pytest diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 23a50041..62ac06a7 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import re import pytz import pytest diff --git a/tests/test_marshalling.py b/tests/test_marshalling.py index 0698c071..e791961b 100644 --- a/tests/test_marshalling.py +++ b/tests/test_marshalling.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import pytest from flask_restplus import ( diff --git a/tests/test_model.py b/tests/test_model.py index 677232e0..eb054554 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import copy import pytest diff --git a/tests/test_namespace.py b/tests/test_namespace.py index fbb26778..27c511c6 100644 --- a/tests/test_namespace.py +++ b/tests/test_namespace.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import flask_restplus as restplus from flask_restplus import Namespace, Model, OrderedModel diff --git a/tests/test_payload.py b/tests/test_payload.py index 21738e74..3c1d6253 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import flask_restplus as restplus diff --git a/tests/test_postman.py b/tests/test_postman.py index 2ef46b4e..04a19ea5 100644 --- a/tests/test_postman.py +++ b/tests/test_postman.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import - import json from os.path import join, dirname diff --git a/tests/test_reqparse.py b/tests/test_reqparse.py index db0bdbbf..2f98390f 100644 --- a/tests/test_reqparse.py +++ b/tests/test_reqparse.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import decimal import json import six diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 9d18fc0d..b0191285 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import - import pytest from jsonschema import ValidationError diff --git a/tests/test_swagger.py b/tests/test_swagger.py index 07337088..78babed1 100644 --- a/tests/test_swagger.py +++ b/tests/test_swagger.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import pytest from textwrap import dedent diff --git a/tests/test_swagger_utils.py b/tests/test_swagger_utils.py index 1f7802a3..9fad4220 100644 --- a/tests/test_swagger_utils.py +++ b/tests/test_swagger_utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from flask_restplus.swagger import extract_path, extract_path_params, parse_docstring diff --git a/tests/test_utils.py b/tests/test_utils.py index c648ba36..8dc0b697 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import pytest from flask_restplus import utils From 7a9a49605d1a4cdf906e46d1a639016565303d89 Mon Sep 17 00:00:00 2001 From: Sam Pegler Date: Thu, 9 Jan 2020 23:01:08 +0000 Subject: [PATCH 2/4] Partial: Remove six. --- flask_restplus/api.py | 13 +++----- flask_restplus/fields.py | 27 ++++++++--------- flask_restplus/inputs.py | 2 +- flask_restplus/marshalling.py | 9 ++---- flask_restplus/mask.py | 15 +++------ flask_restplus/model.py | 13 +++----- flask_restplus/namespace.py | 7 ++--- flask_restplus/postman.py | 19 ++++++------ flask_restplus/reqparse.py | 20 +++++------- flask_restplus/swagger.py | 54 ++++++++++++++------------------- flask_restplus/utils.py | 13 +++----- requirements/install.pip | 2 -- tests/legacy/test_api_legacy.py | 3 +- tests/test_inputs.py | 11 +++---- tests/test_postman.py | 2 +- tests/test_reqparse.py | 14 ++++----- 16 files changed, 87 insertions(+), 137 deletions(-) diff --git a/flask_restplus/api.py b/flask_restplus/api.py index 9ff93ae5..399a909c 100644 --- a/flask_restplus/api.py +++ b/flask_restplus/api.py @@ -4,14 +4,9 @@ import logging import operator import re -import six import sys -try: - from collections.abc import OrderedDict -except ImportError: - # TODO Remove this to drop Python2 support - from collections import OrderedDict +from collections import OrderedDict from functools import wraps, partial from types import MethodType @@ -436,7 +431,7 @@ def add_namespace(self, ns, path=None): urls = self.ns_urls(ns, r.urls) self.register_resource(ns, r.resource, *urls, **r.kwargs) # Register models - for name, definition in six.iteritems(ns.models): + for name, definition in ns.models.items(): self.models[name] = definition if not self.blueprint and self.app is not None: self._configure_namespace_logger(self.app, ns) @@ -508,7 +503,7 @@ def _own_and_child_error_handlers(self): rv = {} rv.update(self.error_handlers) for ns in self.namespaces: - for exception, handler in six.iteritems(ns.error_handlers): + for exception, handler in ns.error_handlers.items(): rv[exception] = handler return rv @@ -623,7 +618,7 @@ def handle_error(self, e): headers = Headers() - for typecheck, handler in six.iteritems(self._own_and_child_error_handlers): + for typecheck, handler in self._own_and_child_error_handlers.items(): if isinstance(e, typecheck): result = handler(e) default_data, code, headers = unpack(result, HTTPStatus.INTERNAL_SERVER_ERROR) diff --git a/flask_restplus/fields.py b/flask_restplus/fields.py index 3041be17..6e60f658 100644 --- a/flask_restplus/fields.py +++ b/flask_restplus/fields.py @@ -7,8 +7,7 @@ from decimal import Decimal, ROUND_HALF_EVEN from email.utils import formatdate -from six import iteritems, itervalues, text_type, string_types -from six.moves.urllib.parse import urlparse, urlunparse +from urllib.parse import urlparse, urlunparse from flask import url_for, request from werkzeug.utils import cached_property @@ -32,7 +31,7 @@ class MarshallingError(RestError): def __init__(self, underlying_exception): # just put the contextual representation of the error to hint on what # went wrong without exposing internals - super(MarshallingError, self).__init__(text_type(underlying_exception)) + super(MarshallingError, self).__init__(str(underlying_exception)) def is_indexable_but_not_string(obj): @@ -363,9 +362,7 @@ def schema(self): class String(StringMixin, Raw): ''' - Marshal a value as a string. Uses ``six.text_type`` so values will - be converted to :class:`unicode` in python2 and :class:`str` in - python3. + Marshal a value as a string. ''' def __init__(self, *args, **kwargs): self.enum = kwargs.pop('enum', None) @@ -375,7 +372,7 @@ def __init__(self, *args, **kwargs): def format(self, value): try: - return text_type(value) + return str(value) except ValueError as ve: raise MarshallingError(ve) @@ -428,7 +425,7 @@ class Arbitrary(NumberMixin, Raw): ''' def format(self, value): - return text_type(Decimal(value)) + return str(Decimal(value)) ZERO = Decimal() @@ -446,7 +443,7 @@ def format(self, value): dvalue = Decimal(value) if not dvalue.is_normal() and dvalue != ZERO: raise MarshallingError('Invalid Fixed precision number.') - return text_type(dvalue.quantize(self.precision, rounding=ROUND_HALF_EVEN)) + return str(dvalue.quantize(self.precision, rounding=ROUND_HALF_EVEN)) class Boolean(Raw): @@ -481,7 +478,7 @@ def __init__(self, dt_format='iso8601', **kwargs): def parse(self, value): if value is None: return None - elif isinstance(value, string_types): + elif isinstance(value, str): parser = datetime_from_iso8601 if self.dt_format == 'iso8601' else datetime_from_rfc822 return parser(value) elif isinstance(value, datetime): @@ -550,7 +547,7 @@ def __init__(self, **kwargs): def parse(self, value): if value is None: return None - elif isinstance(value, string_types): + elif isinstance(value, str): return date_from_iso8601(value) elif isinstance(value, datetime): return value.date() @@ -609,7 +606,7 @@ class FormattedString(StringMixin, Raw): ''' def __init__(self, src_str, **kwargs): super(FormattedString, self).__init__(**kwargs) - self.src_str = text_type(src_str) + self.src_str = str(src_str) def output(self, key, obj, **kwargs): try: @@ -657,7 +654,7 @@ class Polymorph(Nested): ''' def __init__(self, mapping, required=False, **kwargs): self.mapping = mapping - parent = self.resolve_ancestor(list(itervalues(mapping))) + parent = self.resolve_ancestor(list(mapping.values())) super(Polymorph, self).__init__(parent, allow_null=not required, **kwargs) def output(self, key, obj, ordered=False, **kwargs): @@ -673,7 +670,7 @@ def output(self, key, obj, ordered=False, **kwargs): if not hasattr(value, '__class__'): raise ValueError('Polymorph field only accept class instances') - candidates = [fields for cls, fields in iteritems(self.mapping) if type(value) == cls] + candidates = [fields for cls, fields in self.mapping.items() if type(value) == cls] if len(candidates) <= 0: raise ValueError('Unknown class: ' + value.__class__.__name__) @@ -738,7 +735,7 @@ def _flatten(self, obj): if obj == self._obj and self._flat is not None: return self._flat if isinstance(obj, dict): - self._flat = [x for x in iteritems(obj)] + self._flat = [x for x in obj.items()] else: def __match_attributes(attribute): diff --git a/flask_restplus/inputs.py b/flask_restplus/inputs.py index d236b3de..622ccfc4 100644 --- a/flask_restplus/inputs.py +++ b/flask_restplus/inputs.py @@ -22,7 +22,7 @@ def my_type(value): from datetime import datetime, time, timedelta from email.utils import parsedate_tz, mktime_tz -from six.moves.urllib.parse import urlparse +from urllib.parse import urlparse import aniso8601 import pytz diff --git a/flask_restplus/marshalling.py b/flask_restplus/marshalling.py index 2cde2b34..81b19ea0 100644 --- a/flask_restplus/marshalling.py +++ b/flask_restplus/marshalling.py @@ -1,10 +1,5 @@ -try: - from collections.abc import OrderedDict -except ImportError: - # TODO Remove this to drop Python2 support - from collections import OrderedDict +from collections import OrderedDict from functools import wraps -from six import iteritems from flask import request, current_app, has_app_context @@ -178,7 +173,7 @@ def __format_field(key, val): (k, marshal(data, v, skip_none=skip_none, ordered=ordered)) if isinstance(v, dict) else __format_field(k, v) - for k, v in iteritems(fields) + for k, v in fields.items() ) if skip_none: diff --git a/flask_restplus/mask.py b/flask_restplus/mask.py index 5d3a5cad..4a7c4b64 100644 --- a/flask_restplus/mask.py +++ b/flask_restplus/mask.py @@ -1,12 +1,7 @@ import logging import re -import six -try: - from collections.abc import OrderedDict -except ImportError: - # TODO Remove this to drop Python2 support - from collections import OrderedDict +from collections import OrderedDict from inspect import isclass from .errors import RestError @@ -35,7 +30,7 @@ class Mask(OrderedDict): ''' def __init__(self, mask=None, skip=False, **kwargs): self.skip = skip - if isinstance(mask, six.string_types): + if isinstance(mask, str): super(Mask, self).__init__() self.parse(mask) elif isinstance(mask, (dict, OrderedDict)): @@ -138,7 +133,7 @@ def filter_data(self, data): ''' out = {} - for field, content in six.iteritems(self): + for field, content in self.items(): if field == '*': continue elif isinstance(content, Mask): @@ -155,7 +150,7 @@ def filter_data(self, data): out[field] = data.get(field, None) if '*' in self.keys(): - for key, value in six.iteritems(data): + for key, value in data.items(): if key not in out: out[key] = value return out @@ -163,7 +158,7 @@ def filter_data(self, data): def __str__(self): return '{{{0}}}'.format(','.join([ ''.join((k, str(v))) if isinstance(v, Mask) else k - for k, v in six.iteritems(self) + for k, v in self.items() ])) diff --git a/flask_restplus/model.py b/flask_restplus/model.py index 993377c3..a0c46acd 100644 --- a/flask_restplus/model.py +++ b/flask_restplus/model.py @@ -2,12 +2,7 @@ import re import warnings -try: - from collections.abc import OrderedDict, MutableMapping -except ImportError: - # TODO Remove this to drop Python2 support - from collections import OrderedDict, MutableMapping -from six import iteritems, itervalues +from collections import OrderedDict, MutableMapping from werkzeug.utils import cached_property from .mask import Mask @@ -143,7 +138,7 @@ def _schema(self): properties = self.wrapper() required = set() discriminator = None - for name, field in iteritems(self): + for name, field in self.items(): field = instance(field) properties[name] = field.__schema__ if field.required: @@ -172,7 +167,7 @@ def resolved(self): resolved.update(parent.resolved) # Handle discriminator - candidates = [f for f in itervalues(resolved) if getattr(f, 'discriminator', None)] + candidates = [f for f in resolved.values() if getattr(f, 'discriminator', None)] # Ensure the is only one discriminator if len(candidates) > 1: raise ValueError('There can only be one discriminator by schema') @@ -220,7 +215,7 @@ def clone(cls, name, *parents): def __deepcopy__(self, memo): obj = self.__class__(self.name, - [(key, copy.deepcopy(value, memo)) for key, value in iteritems(self)], + [(key, copy.deepcopy(value, memo)) for key, value in self.items()], mask=self.__mask__) obj.__parents__ = self.__parents__ return obj diff --git a/flask_restplus/namespace.py b/flask_restplus/namespace.py index 1c9fc52d..58b44b67 100644 --- a/flask_restplus/namespace.py +++ b/flask_restplus/namespace.py @@ -3,7 +3,6 @@ import logging from collections import namedtuple -import six from flask import request from flask.views import http_method_funcs @@ -115,7 +114,7 @@ def _build_doc(self, cls, doc): def doc(self, shortcut=None, **kwargs): '''A decorator to add some api documentation to the decorated object''' - if isinstance(shortcut, six.text_type): + if isinstance(shortcut, str): kwargs['id'] = shortcut show = shortcut if isinstance(shortcut, bool) else True @@ -331,8 +330,8 @@ def payload(self): def unshortcut_params_description(data): if 'params' in data: - for name, description in six.iteritems(data['params']): - if isinstance(description, six.string_types): + for name, description in data['params'].items(): + if isinstance(description, str): data['params'][name] = {'description': description} diff --git a/flask_restplus/postman.py b/flask_restplus/postman.py index 38d48245..0ab2487d 100644 --- a/flask_restplus/postman.py +++ b/flask_restplus/postman.py @@ -1,13 +1,12 @@ from time import time from uuid import uuid5, NAMESPACE_URL -from six import iteritems -from six.moves.urllib.parse import urlencode +from urllib.parse import urlencode def clean(data): '''Remove all keys where value is None''' - return dict((k, v) for k, v in iteritems(data) if v is not None) + return dict((k, v) for k, v in data.items() if v is not None) DEFAULT_VARS = { @@ -52,15 +51,15 @@ def headers(self): # Add security headers if needed (global then local) for security in self.collection.api.__schema__.get('security', []): - for key, header in iteritems(self.collection.apikeys): + for key, header in self.collection.apikeys.items(): if key in security: headers[header] = '' for security in self.operation.get('security', []): - for key, header in iteritems(self.collection.apikeys): + for key, header in self.collection.apikeys.items(): if key in security: headers[header] = '' - lines = [':'.join(line) for line in iteritems(headers)] + lines = [':'.join(line) for line in headers.items()] return '\n'.join(lines) @property @@ -95,7 +94,7 @@ def process_url(self, urlvars=False): params.update(dict((p['name'], p) for p in self.operation.get('parameters', []))) if not params: return url, None - for name, param in iteritems(params): + for name, param in params.items(): if param['in'] == 'path': url = url.replace('{%s}' % name, ':%s' % name) path_vars[name] = DEFAULT_VARS.get(param['type'], '') @@ -157,10 +156,10 @@ def requests(self): 'summary': 'The API Swagger specifications as JSON', }) # Then iter over API paths and methods - for path, operations in iteritems(self.api.__schema__['paths']): + for path, operations in self.api.__schema__['paths'].items(): path_params = operations.get('parameters', []) - for method, operation in iteritems(operations): + for method, operation in operations.items(): if method != 'parameters': yield Request(self, path, path_params, method, operation) @@ -173,7 +172,7 @@ def folders(self): def apikeys(self): return dict( (name, secdef['name']) - for name, secdef in iteritems(self.api.__schema__.get('securityDefinitions')) + for name, secdef in self.api.__schema__.get('securityDefinitions').items() if secdef.get('in') == 'header' and secdef.get('type') == 'apiKey' ) diff --git a/flask_restplus/reqparse.py b/flask_restplus/reqparse.py index fed31ac6..f4bcf700 100644 --- a/flask_restplus/reqparse.py +++ b/flask_restplus/reqparse.py @@ -1,10 +1,6 @@ import decimal -import six -try: - from collections.abc import Hashable -except ImportError: - from collections import Hashable +from collections import Hashable from copy import deepcopy from flask import current_app, request @@ -62,8 +58,6 @@ def __setattr__(self, name, value): SPLIT_CHAR = ',' -text_type = lambda x: six.text_type(x) # noqa - class Argument(object): ''' @@ -77,7 +71,7 @@ class Argument(object): :param bool ignore: Whether to ignore cases where the argument fails type conversion :param type: The type to which the request argument should be converted. If a type raises an exception, the message in the error will be returned in the response. - Defaults to :class:`unicode` in python2 and :class:`str` in python3. + Defaults to :class:`str`. :param location: The attributes of the :class:`flask.Request` object to source the arguments from (ex: headers, args, etc.), can be an iterator. The last item listed takes precedence in the result set. @@ -95,7 +89,7 @@ class Argument(object): ''' def __init__(self, name, default=None, dest=None, required=False, - ignore=False, type=text_type, location=('json', 'values',), + ignore=False, type=str, location=('json', 'values',), choices=(), action='store', help=None, operators=('=',), case_sensitive=True, store_missing=True, trim=False, nullable=True): @@ -120,7 +114,7 @@ def source(self, request): Pulls values off the request in the provided location :param request: The flask request object to parse arguments from ''' - if isinstance(self.location, six.string_types): + if isinstance(self.location, str): value = getattr(request, self.location, MultiDict()) if callable(value): value = value() @@ -174,8 +168,8 @@ def handle_validation_error(self, error, bundle_errors): dict with the name of the argument and the error message to be bundled ''' - error_str = six.text_type(error) - error_msg = ' '.join([six.text_type(self.help), error_str]) if self.help else error_str + error_str = str(error) + error_msg = ' '.join([str(self.help), error_str]) if self.help else error_str errors = {self.name: error_msg} if bundle_errors: @@ -238,7 +232,7 @@ def parse(self, request, bundle_errors=False): results.append(value) if not results and self.required: - if isinstance(self.location, six.string_types): + if isinstance(self.location, str): location = _friendly_location.get(self.location, self.location) else: locations = [_friendly_location.get(loc, loc) for loc in self.location] diff --git a/flask_restplus/swagger.py b/flask_restplus/swagger.py index 43c3a391..4cbd2474 100644 --- a/flask_restplus/swagger.py +++ b/flask_restplus/swagger.py @@ -2,12 +2,7 @@ import re from inspect import isclass, getdoc -try: - from collections.abc import OrderedDict, Hashable -except ImportError: - # TODO Remove this to drop Python2 support - from collections import OrderedDict, Hashable -from six import string_types, itervalues, iteritems, iterkeys +from collections import OrderedDict, Hashable from flask import current_app from werkzeug.routing import parse_rule @@ -18,10 +13,7 @@ from .utils import merge, not_none, not_none_sorted from ._http import HTTPStatus -try: - from urllib.parse import quote -except ImportError: - from urllib import quote +from urllib.parse import quote #: Maps Flask/Werkzeug rooting types to Swagger ones PATH_TYPES = { @@ -98,7 +90,7 @@ def _param_to_header(param): def _clean_header(header): - if isinstance(header, string_types): + if isinstance(header, str): header = {'description': header} typedef = header.get('type', 'string') if isinstance(typedef, Hashable) and typedef in PY_TYPES: @@ -212,7 +204,7 @@ def as_dict(self): 'basePath': basepath, 'paths': not_none_sorted(paths), 'info': infos, - 'produces': list(iterkeys(self.api.representations)), + 'produces': list(self.api.representations.keys()), 'consumes': ['application/json'], 'securityDefinitions': self.api.authorizations or None, 'security': self.security_requirements(self.api.security) or None, @@ -233,7 +225,7 @@ def extract_tags(self, api): tags = [] by_name = {} for tag in api.tags: - if isinstance(tag, string_types): + if isinstance(tag, str): tag = {'name': tag} elif isinstance(tag, (list, tuple)): tag = {'name': tag[0], 'description': tag[1]} @@ -296,7 +288,7 @@ def extract_resource_doc(self, resource, url, route_doc=None): method_doc['docstring'] = parse_docstring(method_impl) method_params = self.expected_params(method_doc) method_params = merge(method_params, method_doc.get('params', {})) - inherited_params = OrderedDict((k, v) for k, v in iteritems(params) if k in method_params) + inherited_params = OrderedDict((k, v) for k, v in params.items() if k in method_params) method_doc['params'] = merge(inherited_params, method_params) for name, param in method_doc['params'].items(): key = (name, param.get('in', 'query')) @@ -361,7 +353,7 @@ def expected_params(self, doc): def register_errors(self): responses = {} - for exception, handler in iteritems(self.api.error_handlers): + for exception, handler in self.api.error_handlers.items(): doc = parse_docstring(handler) response = { 'description': doc['summary'] @@ -423,7 +415,7 @@ def vendor_fields(self, doc, method): ''' return dict( (k if k.startswith('x-') else 'x-{0}'.format(k), v) - for k, v in iteritems(doc[method].get('vendor', {})) + for k, v in doc[method].get('vendor', {}).items() ) def description_for(self, doc, method): @@ -444,7 +436,7 @@ def operation_id_for(self, doc, method): def parameters_for(self, doc): params = [] - for name, param in iteritems(doc['params']): + for name, param in doc['params'].items(): param['name'] = name if 'type' not in param and 'schema' not in param: param['type'] = 'string' @@ -473,7 +465,7 @@ def parameters_for(self, doc): 'format': 'mask', 'description': 'An optional fields mask', } - if isinstance(mask, string_types): + if isinstance(mask, str): param['default'] = mask params.append(param) @@ -485,9 +477,9 @@ def responses_for(self, doc, method): for d in doc, doc[method]: if 'responses' in d: - for code, response in iteritems(d['responses']): + for code, response in d['responses'].items(): code = str(code) - if isinstance(response, string_types): + if isinstance(response, str): description = response model = None kwargs = {} @@ -517,8 +509,8 @@ def responses_for(self, doc, method): responses[code]['schema'] = self.serialize_schema(d['model']) if 'docstring' in d: - for name, description in iteritems(d['docstring']['raises']): - for exception, handler in iteritems(self.api.error_handlers): + for name, description in d['docstring']['raises'].items(): + for exception, handler in self.api.error_handlers.items(): error_responses = getattr(handler, '__apidoc__', {}).get('responses', {}) code = str(list(error_responses.keys())[0]) if error_responses else None if code and exception.__name__ == name: @@ -535,9 +527,9 @@ def process_headers(self, response, doc, method=None, headers=None): response['headers'] = dict( (k, _clean_header(v)) for k, v in itertools.chain( - iteritems(doc.get('headers', {})), - iteritems(method_doc.get('headers', {})), - iteritems(headers or {}) + doc.get('headers', {}).items(), + method_doc.get('headers', {}).items(), + headers.items() if headers else dict().items() ) ) return response @@ -545,7 +537,7 @@ def process_headers(self, response, doc, method=None, headers=None): def serialize_definitions(self): return dict( (name, model.__schema__) - for name, model in iteritems(self._registered_models) + for name, model in self._registered_models.items() ) def serialize_schema(self, model): @@ -560,7 +552,7 @@ def serialize_schema(self, model): self.register_model(model) return ref(model) - elif isinstance(model, string_types): + elif isinstance(model, str): self.register_model(model) return ref(model) @@ -585,13 +577,13 @@ def register_model(self, model): for parent in specs.__parents__: self.register_model(parent) if isinstance(specs, Model): - for field in itervalues(specs): + for field in specs.values(): self.register_field(field) return ref(model) def register_field(self, field): if isinstance(field, fields.Polymorph): - for model in itervalues(field.mapping): + for model in field.mapping.values(): self.register_model(model) elif isinstance(field, fields.Nested): self.register_model(field.nested) @@ -620,12 +612,12 @@ def security_requirements(self, value): return [] def security_requirement(self, value): - if isinstance(value, (string_types)): + if isinstance(value, (str)): return {value: []} elif isinstance(value, dict): return dict( (k, v if isinstance(v, (list, tuple)) else [v]) - for k, v in iteritems(value) + for k, v in value.items() ) else: return None diff --git a/flask_restplus/utils.py b/flask_restplus/utils.py index b408b719..a08b1563 100644 --- a/flask_restplus/utils.py +++ b/flask_restplus/utils.py @@ -1,12 +1,7 @@ import re -try: - from collections.abc import OrderedDict -except ImportError: - # TODO Remove this to drop Python2 support - from collections import OrderedDict +from collections import OrderedDict from copy import deepcopy -from six import iteritems from ._http import HTTPStatus @@ -33,7 +28,7 @@ def merge(first, second): if not isinstance(second, dict): return second result = deepcopy(first) - for key, value in iteritems(second): + for key, value in second.items(): if key in result and isinstance(result[key], dict): result[key] = merge(result[key], value) else: @@ -66,7 +61,7 @@ def not_none(data): :return: The same dictionary without the keys with values to ``None`` :rtype: dict ''' - return dict((k, v) for k, v in iteritems(data) if v is not None) + return dict((k, v) for k, v in data.items() if v is not None) def not_none_sorted(data): @@ -77,7 +72,7 @@ def not_none_sorted(data): :return: The same dictionary without the keys with values to ``None`` :rtype: OrderedDict ''' - return OrderedDict((k, v) for k, v in sorted(iteritems(data)) if v is not None) + return OrderedDict((k, v) for k, v in sorted(data.items()) if v is not None) def unpack(response, default_code=HTTPStatus.OK): diff --git a/requirements/install.pip b/requirements/install.pip index fc6c94dc..803c897c 100644 --- a/requirements/install.pip +++ b/requirements/install.pip @@ -2,5 +2,3 @@ aniso8601>=0.82 jsonschema Flask>=0.8 pytz -six>=1.3.0 -enum34; python_version < '3.4' diff --git a/tests/legacy/test_api_legacy.py b/tests/legacy/test_api_legacy.py index 88d0b8a8..636aa7f6 100644 --- a/tests/legacy/test_api_legacy.py +++ b/tests/legacy/test_api_legacy.py @@ -3,7 +3,6 @@ import flask import pytest -import six from json import dumps, JSONEncoder @@ -254,7 +253,7 @@ def test_resource_resp(self, app, mocker): def test_resource_text_plain(self, app): def text(data, code, headers=None): - return flask.make_response(six.text_type(data)) + return flask.make_response(str(data)) class Foo(restplus.Resource): representations = { diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 62ac06a7..dd1627e0 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -3,7 +3,6 @@ import pytest from datetime import date, datetime -from six import text_type from flask_restplus import inputs @@ -97,9 +96,9 @@ def assert_bad_url(self, validator, value, details=None): with pytest.raises(ValueError) as cm: validator(value) if details: - assert text_type(cm.value) == '. '.join((msg, details)).format(value) + assert str(cm.value) == '. '.join((msg, details)).format(value) else: - assert text_type(cm.value).startswith(msg.format(value)) + assert str(cm.value).startswith(msg.format(value)) @pytest.mark.parametrize('url', [ 'http://www.djangoproject.com/', @@ -142,7 +141,7 @@ def test_bad_urls(self, url): # msg = '{0} is not a valid URL'.format(url) # with pytest.raises(ValueError) as cm: # validator(url) - # assert text_type(cm.exception).startswith(msg) + # assert str(cm.exception).startswith(msg) @pytest.mark.parametrize('url', [ 'google.com', @@ -343,7 +342,7 @@ def test_valid_url(self, url): def test_bad_url(self, url): with pytest.raises(ValueError) as cm: inputs.url(url) - assert text_type(cm.value).startswith('{0} is not a valid URL'.format(url)) + assert str(cm.value).startswith('{0} is not a valid URL'.format(url)) @pytest.mark.parametrize('url', [ 'google.com', @@ -354,7 +353,7 @@ def test_bad_url(self, url): def test_bad_url_with_suggestion(self, url): with pytest.raises(ValueError) as cm: inputs.url(url) - assert text_type(cm.value) == '{0} is not a valid URL. Did you mean: http://{0}'.format(url) + assert str(cm.value) == '{0} is not a valid URL. Did you mean: http://{0}'.format(url) def test_schema(self): assert inputs.url.__schema__ == {'type': 'string', 'format': 'url'} diff --git a/tests/test_postman.py b/tests/test_postman.py index 04a19ea5..5d133b96 100644 --- a/tests/test_postman.py +++ b/tests/test_postman.py @@ -7,7 +7,7 @@ import flask_restplus as restplus -from six.moves.urllib.parse import parse_qs, urlparse +from urllib.parse import parse_qs, urlparse with open(join(dirname(__file__), 'postman-v1.schema.json')) as f: diff --git a/tests/test_reqparse.py b/tests/test_reqparse.py index 2f98390f..5864068d 100644 --- a/tests/test_reqparse.py +++ b/tests/test_reqparse.py @@ -1,6 +1,6 @@ import decimal import json -import six +import io import pytest from werkzeug.exceptions import BadRequest @@ -483,7 +483,7 @@ def test_type_filestorage(self, app): fdata = 'foo bar baz qux'.encode('utf-8') with app.test_request_context('/bubble', method='POST', - data={'foo': (six.BytesIO(fdata), 'baz.txt')}): + data={'foo': (io.BytesIO(fdata), 'baz.txt')}): args = parser.parse_args() assert args['foo'].name == 'foo' @@ -501,7 +501,7 @@ def _custom_type(f): fdata = 'foo bar baz qux'.encode('utf-8') with app.test_request_context('/bubble', method='POST', - data={'foo': (six.BytesIO(fdata), 'baz.txt')}): + data={'foo': (io.BytesIO(fdata), 'baz.txt')}): args = parser.parse_args() assert args['foo'].name == 'fooaaaa' @@ -761,12 +761,10 @@ def test_default_operators(self): assert arg.operators[0] == '=' assert len(arg.operators) == 1 - def test_default_type(self, mocker): - mock_six = mocker.patch('flask_restplus.reqparse.six') + def test_default_type(self): arg = Argument('foo') - sentinel = object() - arg.type(sentinel) - mock_six.text_type.assert_called_with(sentinel) + sentinel = 666 + assert arg.type(sentinel) == '666' def test_default_default(self): arg = Argument('foo') From f3634244464acb0cba8c7e6de913ebde39063efe Mon Sep 17 00:00:00 2001 From: Sam Pegler Date: Thu, 9 Jan 2020 23:01:19 +0000 Subject: [PATCH 3/4] Partial: Clean up setup/tox. --- setup.py | 3 --- tox.ini | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 010a3d65..5fab90fb 100644 --- a/setup.py +++ b/setup.py @@ -96,10 +96,7 @@ def pip(filename): 'Intended Audience :: Developers', 'Topic :: System :: Software Distribution', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', diff --git a/tox.ini b/tox.ini index 62e8cf2a..669ef7de 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py{27,34,35,36,37}, pypy, pypy3, doc +envlist = py{35,36,37}, pypy, pypy3, doc [testenv] commands = {posargs:inv test qa} From 98308a90b6637c9813a95db6bf500c9eecdccd29 Mon Sep 17 00:00:00 2001 From: Sam Pegler Date: Fri, 10 Jan 2020 12:18:02 +0000 Subject: [PATCH 4/4] Partial: Remove a few stragglers. --- doc/parsing.rst | 2 +- flask_restplus/inputs.py | 2 -- flask_restplus/schemas/__init__.py | 9 +-------- tasks.py | 3 --- tests/legacy/test_api_legacy.py | 3 --- tests/legacy/test_api_with_blueprint.py | 3 --- tests/test_fields.py | 6 +----- tests/test_fields_mask.py | 6 +----- tests/test_marshalling.py | 6 +----- tests/test_model.py | 6 +----- 10 files changed, 6 insertions(+), 40 deletions(-) diff --git a/doc/parsing.rst b/doc/parsing.rst index 926a9f87..f7867ebc 100644 --- a/doc/parsing.rst +++ b/doc/parsing.rst @@ -38,7 +38,7 @@ It looks for two arguments in the :attr:`flask.Request.values` dict: an integer .. note :: The default argument type is a unicode string. - This will be ``str`` in python3 and ``unicode`` in python2. + This will be ``str``. If you specify the ``help`` value, it will be rendered as the error message when a type error is raised while parsing it. diff --git a/flask_restplus/inputs.py b/flask_restplus/inputs.py index 622ccfc4..743b60fa 100644 --- a/flask_restplus/inputs.py +++ b/flask_restplus/inputs.py @@ -15,8 +15,6 @@ def my_type(value): The last line allows you to document properly the type in the Swagger documentation. ''' -from __future__ import unicode_literals - import re import socket diff --git a/flask_restplus/schemas/__init__.py b/flask_restplus/schemas/__init__.py index 90e5fc97..3d5674bf 100644 --- a/flask_restplus/schemas/__init__.py +++ b/flask_restplus/schemas/__init__.py @@ -1,21 +1,14 @@ -# -*- coding: utf-8 -*- ''' This module give access to OpenAPI specifications schemas and allows to validate specs against them. .. versionadded:: 0.12.1 ''' -from __future__ import unicode_literals - import io import json import pkg_resources -try: - from collections.abc import Mapping -except ImportError: - # TODO Remove this to drop Python2 support - from collections import Mapping +from collections.abc import Mapping from jsonschema import Draft4Validator from flask_restplus import errors diff --git a/tasks.py b/tasks.py index 7c96955c..99322c21 100644 --- a/tasks.py +++ b/tasks.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import - import os import sys diff --git a/tests/legacy/test_api_legacy.py b/tests/legacy/test_api_legacy.py index 636aa7f6..14d8665c 100644 --- a/tests/legacy/test_api_legacy.py +++ b/tests/legacy/test_api_legacy.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import flask import pytest diff --git a/tests/legacy/test_api_with_blueprint.py b/tests/legacy/test_api_with_blueprint.py index bce08d77..5ecd2c37 100644 --- a/tests/legacy/test_api_with_blueprint.py +++ b/tests/legacy/test_api_with_blueprint.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import flask from flask import Blueprint, request diff --git a/tests/test_fields.py b/tests/test_fields.py index bc2ca2c8..b34bea3e 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,8 +1,4 @@ -try: - from collections.abc import OrderedDict -except ImportError: - # TODO Remove this to drop Python2 support - from collections import OrderedDict +from collections import OrderedDict from datetime import date, datetime from decimal import Decimal from functools import partial diff --git a/tests/test_fields_mask.py b/tests/test_fields_mask.py index 1af8c930..1e7a8799 100644 --- a/tests/test_fields_mask.py +++ b/tests/test_fields_mask.py @@ -1,11 +1,7 @@ import json import pytest -try: - from collections.abc import OrderedDict -except ImportError: - # TODO Remove this to drop Python2 support - from collections import OrderedDict +from collections import OrderedDict from flask_restplus import mask, Api, Resource, fields, marshal, Mask diff --git a/tests/test_marshalling.py b/tests/test_marshalling.py index e791961b..c150d481 100644 --- a/tests/test_marshalling.py +++ b/tests/test_marshalling.py @@ -4,11 +4,7 @@ marshal, marshal_with, marshal_with_field, fields, Api, Resource ) -try: - from collections.abc import OrderedDict -except ImportError: - # TODO Remove this to drop Python2 support - from collections import OrderedDict +from collections import OrderedDict # Add a dummy Resource to verify that the app is properly set. diff --git a/tests/test_model.py b/tests/test_model.py index eb054554..e33c550c 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1,11 +1,7 @@ import copy import pytest -try: - from collections.abc import OrderedDict -except ImportError: - # TODO Remove this to drop Python2 support - from collections import OrderedDict +from collections import OrderedDict from flask_restplus import fields, Model, OrderedModel, SchemaModel