diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 168ed583..dca30509 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,12 +7,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - name: Set up Python 3.9 - uses: actions/setup-python@v1 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.10" - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fedc0ca0..61df37ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,8 +15,13 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "pypy3.9", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.11"] flask: ["<3.0.0", ">=3.0.0"] + exclude: + - python-version: "3.13" + flask: "<3.0.0" + - python-version: "3.14" + flask: "<3.0.0" steps: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -28,6 +33,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + # PyPy: pin rpds-py before resolving jsonschema/referencing so pip picks manylinux wheels + # (sdists use maturin; builds are fragile on PyPy). CPython does not need this. + case "${{ matrix.python-version }}" in + pypy3.11) pip install 'rpds-py<0.30' 'readme-renderer>=35,<42' ;; + esac pip install "flask${{ matrix.flask }}" pip install ".[test]" - name: Test with inv @@ -41,10 +51,10 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - - name: Set up Python 3.9 + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: - python-version: "3.9" + python-version: "3.12" - name: Checkout ${{ github.base_ref }} uses: actions/checkout@v3 with: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7e36709d..ea0152da 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,28 @@ Releases prior to 0.3.0 were “best effort” filled out, but are missing some info. If you see your contribution missing info, please open a PR on the Changelog! +.. _section-1.3.3: +1.3.3 +----- +.. _enhancements-1.3.3 +Enhancements +~~~~~~~~~~~~ + +:: + + * Drop support for Python 3.9 (EOL); require Python 3.10 or newer. [python-restx] + * Declare and test support for Python 3.13 and 3.14 (CPython). [python-restx] + * Expand GitHub Actions and tox coverage: PyPy 3.11 with both Flask 2.x and Flask 3.x; exclude Flask 2 on 3.13/3.14 where unsupported. [python-restx] + * Refresh test and release tooling (pytest, pytest-benchmark, pytest-profiling, twine) for newer interpreters. GitHub Actions pre-installs compatible ``rpds-py`` (and ``readme-renderer`` for PyPy 3.11) for PyPy jobs; tox pins ``rpds-py`` for local PyPy envs. [python-restx] + +.. _bug_fixes-1.3.3 +Bug Fixes +~~~~~~~~~ + +:: + + * Adjust field tests for Python 3.14 (``staticmethod`` around ``functools.partial`` used as a class attribute). [python-restx] + .. _section-1.3.1: 1.3.1 ----- diff --git a/README.rst b/README.rst index 39bbac5f..0a68b497 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ and expose its documentation properly using `Swagger`_. Compatibility ============= -Flask-RESTX requires Python 3.9+. +Flask-RESTX requires Python 3.10+. On Flask Compatibility ====================== diff --git a/bumpr.rc b/bumpr.rc index 337c662b..3b025ee4 100644 --- a/bumpr.rc +++ b/bumpr.rc @@ -4,7 +4,7 @@ vcs = git commit = true tag = true push = true -tests = tox -e py39 +tests = tox -e py310 clean = inv clean files = diff --git a/doc/index.rst b/doc/index.rst index cacdb53b..649aadc9 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -33,7 +33,7 @@ development and to support our users. Compatibility ============= -Flask-RESTX requires Python 3.9+. +Flask-RESTX requires Python 3.10+. Installation diff --git a/doc/installation.rst b/doc/installation.rst index c4c34604..9934494d 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -20,5 +20,5 @@ The development version can be downloaded from pip install -e .[dev,test] -Flask-RESTX requires Python version 3.9+. +Flask-RESTX requires Python version 3.10+. It's also working with PyPy and PyPy3. diff --git a/flask_restx/fields.py b/flask_restx/fields.py index 4484ef51..9bbb9c79 100644 --- a/flask_restx/fields.py +++ b/flask_restx/fields.py @@ -22,7 +22,6 @@ from .marshalling import marshal from .utils import camel_to_dash, not_none - __all__ = ( "Raw", "String", @@ -880,10 +879,10 @@ def output(self, key, obj, ordered=False): # complexity to O(n) if ordered: # Get first element if respecting order - (objkey, val) = self._flat.pop(0) + objkey, val = self._flat.pop(0) else: # Previous default retained - (objkey, val) = self._flat.pop() + objkey, val = self._flat.pop() if ( objkey not in self._cache and objkey not in self.exclude diff --git a/flask_restx/model.py b/flask_restx/model.py index b72496cf..f8bcd87e 100644 --- a/flask_restx/model.py +++ b/flask_restx/model.py @@ -17,7 +17,6 @@ from .utils import not_none from ._http import HTTPStatus - RE_REQUIRED = re.compile(r"u?\'(?P.*)\' is a required property", re.I | re.U) diff --git a/flask_restx/utils.py b/flask_restx/utils.py index 409657aa..16a194fd 100644 --- a/flask_restx/utils.py +++ b/flask_restx/utils.py @@ -7,7 +7,6 @@ from ._http import HTTPStatus - FIRST_CAP_RE = re.compile("(.)([A-Z][a-z]+)") ALL_CAP_RE = re.compile("([a-z0-9])([A-Z])") diff --git a/requirements/test.pip b/requirements/test.pip index 6ce09332..9d709f71 100644 --- a/requirements/test.pip +++ b/requirements/test.pip @@ -1,13 +1,12 @@ blinker Faker==2.0.0 mock==3.0.5 -pytest==7.0.1 -pytest-benchmark==3.4.1 +pytest==8.4.2 +pytest-benchmark==5.2.3 pytest-cov==4.0.0 pytest-flask==1.3.0 pytest-mock==3.6.1 -pytest-profiling==1.7.0 +pytest-profiling==1.8.1 invoke==2.2.0 -twine==3.8.0 +twine==6.2.0 setuptools -backports.zoneinfo;python_version<"3.9" diff --git a/setup.py b/setup.py index eed17d83..335c55f7 100644 --- a/setup.py +++ b/setup.py @@ -102,13 +102,14 @@ def pip(filename): "Topic :: System :: Software Distribution", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: BSD License", ], - python_requires=">=3.9", + python_requires=">=3.10", ) diff --git a/tests/test_fields.py b/tests/test_fields.py index 9f255c5c..b57f83c6 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -693,7 +693,7 @@ def test_unsupported_value_format(self): class FormatedStringFieldTest(StringTestMixin, BaseFieldTestMixin, FieldTestCase): - field_class = partial(fields.FormattedString, "Hello {name}") + field_class = staticmethod(partial(fields.FormattedString, "Hello {name}")) def test_defaults(self): field = fields.FormattedString("Hello {name}") @@ -731,7 +731,7 @@ def test_tuple(self): class UrlFieldTest(StringTestMixin, BaseFieldTestMixin, FieldTestCase): - field_class = partial(fields.Url, "endpoint") + field_class = staticmethod(partial(fields.Url, "endpoint")) def test_defaults(self): field = fields.Url("endpoint") @@ -931,7 +931,7 @@ def test_as_list_is_reusable(self, api): class ListFieldTest(BaseFieldTestMixin, FieldTestCase): - field_class = partial(fields.List, fields.String) + field_class = staticmethod(partial(fields.List, fields.String)) def test_defaults(self): field = fields.List(fields.String) @@ -1025,7 +1025,7 @@ def test_list_of_raw(self): class WildcardFieldTest(BaseFieldTestMixin, FieldTestCase): - field_class = partial(fields.Wildcard, fields.String) + field_class = staticmethod(partial(fields.Wildcard, fields.String)) def test_types(self): with pytest.raises(fields.MarshallingError): diff --git a/tests/test_postman.py b/tests/test_postman.py index 0317bf64..85c91628 100644 --- a/tests/test_postman.py +++ b/tests/test_postman.py @@ -9,7 +9,6 @@ from urllib.parse import parse_qs, urlparse - with open(join(dirname(__file__), "postman-v1.schema.json")) as f: schema = json.load(f) diff --git a/tests/test_swagger.py b/tests/test_swagger.py index 8f181505..c4a5ca7b 100644 --- a/tests/test_swagger.py +++ b/tests/test_swagger.py @@ -1451,23 +1451,17 @@ def get(self): description = lambda m: data["paths"]["/description/"][m]["description"] # noqa - assert description("get") == dedent( - """\ + assert description("get") == dedent("""\ Parent description. - Some details""" - ) + Some details""") - assert description("post") == dedent( - """\ + assert description("post") == dedent("""\ Parent description. - Extra description""" - ) + Extra description""") - assert description("delete") == dedent( - """\ + assert description("delete") == dedent("""\ Parent description. - A delete operation""" - ) + A delete operation""") assert description("put") == "Parent description." assert "description" not in data["paths"]["/descriptionless/"]["get"] diff --git a/tox.ini b/tox.ini index 3ac33e79..f28dd779 100644 --- a/tox.ini +++ b/tox.ini @@ -5,9 +5,10 @@ [tox] envlist = - py{39, 310, 311}-flask2, - py{311, 312}-flask3 - pypy3.9 + py{310, 311, 312}-flask2, + py{311, 312, 313, 314}-flask3 + pypy3.11-flask2 + pypy3.11-flask3 doc [testenv] @@ -18,6 +19,12 @@ deps = -r{toxinidir}/requirements/test.pip -r{toxinidir}/requirements/develop.pip +[testenv:pypy3.11-flask{2,3}] +deps = + {[testenv]deps} + rpds-py<0.30 +commands = {posargs:inv test} + [testenv:doc] changedir = doc deps = .[doc]