diff --git a/.github/workflows/manual-release-candidate.yml b/.github/workflows/manual-release-candidate.yml index 2452f7b..1f14d30 100644 --- a/.github/workflows/manual-release-candidate.yml +++ b/.github/workflows/manual-release-candidate.yml @@ -32,7 +32,7 @@ env: DIST_ARTIFACT: python-dist PYTHON_SEMANTIC_RELEASE_VERSION: "7.34.6" PYTHON_VERSION_DEFAULT: "3.11" - POETRY_VERSION: "1.4.1" + POETRY_VERSION: "1.7.1" jobs: release_candidate: diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 5638fa5..db91a2a 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -38,7 +38,7 @@ concurrency: env: REPORTS_DIR: CI_reports PYTHON_VERSION_DEFAULT: "3.11" - POETRY_VERSION: "1.4.1" + POETRY_VERSION: "1.7.1" jobs: coding-standards: @@ -80,10 +80,10 @@ jobs: - # test with the locked dependencies python-version: '3.11' toxenv-factor: 'locked' - - # test with the lowest dependencies python-version: '3.7' toxenv-factor: 'lowest' + poetry-version: '1.4.1' # py3.7 does not allow latest poetry steps: - name: Checkout # see https://github.com/actions/checkout @@ -100,7 +100,7 @@ jobs: # see https://github.com/marketplace/actions/setup-poetry uses: Gr1N/setup-poetry@v8 with: - poetry-version: ${{ env.POETRY_VERSION }} + poetry-version: ${{ matrix.poetry-version || env.POETRY_VERSION }} - name: Install Dependencies run: poetry install --no-interaction --no-root @@ -145,6 +145,10 @@ jobs: python-version: ${{ matrix.python-version }} architecture: 'x64' + - name: override POETRY_VERSION + if: startsWith(matrix.python-version, '3.7') + run: echo "POETRY_VERSION=1.4.1" >> "$GITHUB_ENV" + shell: bash - name: Install poetry # see https://github.com/marketplace/actions/setup-poetry uses: Gr1N/setup-poetry@v8 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 239b10e..2e4f1e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ concurrency: env: PYTHON_VERSION_DEFAULT: "3.11" - POETRY_VERSION: "1.4.1" + POETRY_VERSION: "1.7.1" jobs: release: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23e816d..52ef846 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,10 @@ Pull requests are welcome, but please read this guidelines first. This project uses [poetry]. Have it installed and setup first. +Attention: +Even though this library is designed to be runnable on python>=3.7 +some development-tools require python>=3.8.1 + To install dev-dependencies and tools: ```shell @@ -20,9 +24,12 @@ Get it all applied via: ```shell poetry run isort . -poetry run flake8 serializable/ tests/ +poetry run autopep8 -ir serializable/ tests/ ``` +This project prefers `f'strings'` over `'string'.format()`. +This project prefers `'single quotes'` over `"double quotes"`. + ## Documentation This project uses [Sphinx] to generate documentation which is automatically published to [RTFD][link_rtfd]. diff --git a/pyproject.toml b/pyproject.toml index e5303c5..54b9ae0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,16 +41,22 @@ python = "^3.7" defusedxml = "^0.7.1" [tool.poetry.dev-dependencies] -coverage = "7.2.7" -flake8 = "5.0.4" # last version supporting Python 3.7 -flake8-annotations = "2.9.1" # last version supporting Python 3.7 -flake8-bugbear = "23.3.12" # last version supporting Python 3.7 -flake8-isort = "6.0.0" -isort = "5.11.5" # last version supporting Python 3.7 -mypy = "1.4.1" -tox = "3.28.0" typing-extensions = {version = "4.7.1", python = "<3.8"} +tox = "3.28.0" +coverage = "7.2.7" xmldiff = "2.6.3" +mypy = [ + {version = "1.7.1", python = ">=3.8"}, + {version = "1.4.1", python = "<3.8"} +] +autopep8 = {version = "2.0.4", python = ">=3.8"} +isort = {version = "5.13.2", python = ">=3.8"} +flake8 = { version="6.1.0", python=">=3.8.1" } +flake8-annotations = { version="3.0.1", python=">=3.8.1" } +flake8-bugbear = { version="23.12.2", python=">=3.8.1" } +flake8-isort = {version = "6.1.1", python = ">=3.8"} +flake8-quotes = {version = "3.3.2", python = ">=3.8"} +flake8-use-fstring = {version = "1.4", python = ">=3.8"} [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/serializable/__init__.py b/serializable/__init__.py index 3f7d8d8..55b608e 100644 --- a/serializable/__init__.py +++ b/serializable/__init__.py @@ -28,8 +28,22 @@ from decimal import Decimal from io import StringIO, TextIOBase from json import JSONEncoder -from sys import version_info -from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, TypeVar, Union, cast, overload +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + List, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, + overload, +) from xml.etree.ElementTree import Element, SubElement from defusedxml import ElementTree as SafeElementTree # type: ignore @@ -37,10 +51,15 @@ from .formatters import BaseNameFormatter, CurrentFormatter from .helpers import BaseHelper -if version_info >= (3, 8): - from typing import Literal, Protocol # type:ignore[attr-defined] +if TYPE_CHECKING: + import sys + if sys.version_info >= (3, 8): + from typing import Literal, Protocol # type:ignore[attr-defined] + else: + from typing_extensions import Literal, Protocol # type:ignore[assignment] else: - from typing_extensions import Literal, Protocol # type:ignore[assignment] + from abc import ABC + Protocol = ABC # `Intersection` is still not implemented, so it is interim replaced by Union for any support # see section "Intersection" in https://peps.python.org/pep-0483/ @@ -63,7 +82,7 @@ class ViewType: pass -_F = TypeVar("_F", bound=Callable[..., Any]) +_F = TypeVar('_F', bound=Callable[..., Any]) _T = TypeVar('_T') _E = TypeVar('_E', bound=enum.Enum) @@ -838,7 +857,7 @@ def parse_type_deferred(self) -> None: def _parse_type(self, type_: Any) -> None: self._type_ = type_ = self._handle_forward_ref(t_=type_) - if type(type_) == str: + if type(type_) is str: type_to_parse = str(type_) # Handle types that are quoted strings e.g. 'SortedSet[MyObject]' or 'Optional[SortedSet[MyObject]]' if type_to_parse.startswith('typing.Optional['): @@ -852,30 +871,30 @@ def _parse_type(self, type_: Any) -> None: if match: results = match.groupdict() if results.get('array_type', None) in self._SORTED_CONTAINERS_TYPES: - mapped_array_type = self._SORTED_CONTAINERS_TYPES.get(str(results.get("array_type"))) + mapped_array_type = self._SORTED_CONTAINERS_TYPES.get(str(results.get('array_type'))) self._is_array = True try: # Will load any class already loaded assuming fully qualified name self._type_ = eval(f'{mapped_array_type}[{results.get("array_of")}]') - self._concrete_type = eval(str(results.get("array_of"))) + self._concrete_type = eval(str(results.get('array_of'))) except NameError: # Likely a class that is missing its fully qualified name _k: Optional[Any] = None for _k_name, _oml_sc in ObjectMetadataLibrary.klass_mappings.items(): - if _oml_sc.name == results.get("array_of"): + if _oml_sc.name == results.get('array_of'): _k = _oml_sc.klass if _k is None: # Perhaps a custom ENUM? for _enum_klass in ObjectMetadataLibrary.custom_enum_klasses: - if _enum_klass.__name__ == results.get("array_of"): + if _enum_klass.__name__ == results.get('array_of'): _k = _enum_klass if _k is None: self._type_ = type_ # type: ignore self._deferred_type_parsing = True ObjectMetadataLibrary.defer_property_type_parsing( - prop=self, klasses=[str(results.get("array_of"))] + prop=self, klasses=[str(results.get('array_of'))] ) return @@ -890,25 +909,25 @@ def _parse_type(self, type_: Any) -> None: try: # Will load any class already loaded assuming fully qualified name self._type_ = eval(f'{mapped_array_type}[{results.get("array_of")}]') - self._concrete_type = eval(str(results.get("array_of"))) + self._concrete_type = eval(str(results.get('array_of'))) except NameError: # Likely a class that is missing its fully qualified name _l: Optional[Any] = None for _k_name, _oml_sc in ObjectMetadataLibrary.klass_mappings.items(): - if _oml_sc.name == results.get("array_of"): + if _oml_sc.name == results.get('array_of'): _l = _oml_sc.klass if _l is None: # Perhaps a custom ENUM? for _enum_klass in ObjectMetadataLibrary.custom_enum_klasses: - if _enum_klass.__name__ == results.get("array_of"): + if _enum_klass.__name__ == results.get('array_of'): _l = _enum_klass if _l is None: self._type_ = type_ # type: ignore self._deferred_type_parsing = True ObjectMetadataLibrary.defer_property_type_parsing( - prop=self, klasses=[str(results.get("array_of"))] + prop=self, klasses=[str(results.get('array_of'))] ) return @@ -945,7 +964,7 @@ def _parse_type(self, type_: Any) -> None: def _handle_forward_ref(self, t_: Any) -> Any: if 'ForwardRef' in str(t_): - return str(t_).replace('ForwardRef(\'', '"').replace('\')', '"') + return str(t_).replace("ForwardRef('", '"').replace("')", '"') else: return t_ @@ -1116,7 +1135,7 @@ def register_property_type_mapping(cls, qual_name: str, mapped_type: type) -> No @overload -def serializable_enum(cls: Literal[None] = None) -> Callable[[Type[_E]], Type[_E]]: +def serializable_enum(cls: 'Literal[None]' = None) -> Callable[[Type[_E]], Type[_E]]: ... @@ -1146,7 +1165,7 @@ def decorate(kls: Type[_E]) -> Type[_E]: @overload def serializable_class( - cls: Literal[None] = None, *, + cls: 'Literal[None]' = None, *, name: Optional[str] = ..., serialization_types: Optional[Iterable[SerializationType]] = ..., ignore_during_deserialization: Optional[Iterable[str]] = ... diff --git a/serializable/formatters.py b/serializable/formatters.py index 6a8a72a..2925724 100644 --- a/serializable/formatters.py +++ b/serializable/formatters.py @@ -96,4 +96,4 @@ def decode(cls, property_name: str) -> str: class CurrentFormatter: - formatter: Type["BaseNameFormatter"] = CamelCasePropertyNameFormatter + formatter: Type['BaseNameFormatter'] = CamelCasePropertyNameFormatter diff --git a/serializable/helpers.py b/serializable/helpers.py index 4560587..4d3f034 100644 --- a/serializable/helpers.py +++ b/serializable/helpers.py @@ -191,7 +191,7 @@ def deserialize(cls, o: Any) -> datetime: o = str(o)[1:] # Ensure any milliseconds are 6 digits - o = re.sub(r"\.(\d{1,6})", lambda v: f'.{int(v.group()[1:]):06}', str(o)) + o = re.sub(r'\.(\d{1,6})', lambda v: f'.{int(v.group()[1:]):06}', str(o)) if str(o).endswith('Z'): # Replace ZULU time with 00:00 offset diff --git a/tests/model.py b/tests/model.py index d8cd537..bcb9189 100644 --- a/tests/model.py +++ b/tests/model.py @@ -67,9 +67,9 @@ def serialize(cls, o: Any) -> Set[str]: raise ValueError(f'Attempt to serialize a non-set: {o.__class__}') @classmethod - def deserialize(cls, o: Any) -> Set["BookReference"]: + def deserialize(cls, o: Any) -> Set['BookReference']: print(f'Deserializing {o} ({type(o)})') - references: Set["BookReference"] = set() + references: Set['BookReference'] = set() if isinstance(o, list): for v in o: references.add(BookReference(ref=v)) @@ -141,7 +141,7 @@ def address(self) -> Optional[str]: @property @serializable.include_none(SchemaVersion2) - @serializable.include_none(SchemaVersion3, "RUBBISH") + @serializable.include_none(SchemaVersion3, 'RUBBISH') def email(self) -> Optional[str]: return self._email @@ -189,7 +189,7 @@ def __hash__(self) -> int: @serializable.serializable_class class BookReference: - def __init__(self, *, ref: str, references: Optional[Iterable["BookReference"]] = None) -> None: + def __init__(self, *, ref: str, references: Optional[Iterable['BookReference']] = None) -> None: self.ref = ref self.references = set(references or {}) @@ -207,11 +207,11 @@ def ref(self, ref: str) -> None: @serializable.json_name('refersTo') @serializable.type_mapping(ReferenceReferences) @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'reference') - def references(self) -> Set["BookReference"]: + def references(self) -> Set['BookReference']: return self._references @references.setter - def references(self, references: Iterable["BookReference"]) -> None: + def references(self, references: Iterable['BookReference']) -> None: self._references = set(references) def __eq__(self, other: object) -> bool: diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 8fb650d..8ad69e0 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -28,24 +28,24 @@ class TestIso8601Date(TestCase): def test_serialize_date(self) -> None: self.assertEqual( Iso8601Date.serialize(o=date(year=2022, month=8, day=3)), - "2022-08-03" + '2022-08-03' ) def test_serialize_datetime(self) -> None: self.assertEqual( Iso8601Date.serialize(o=datetime(year=2022, month=8, day=3)), - "2022-08-03" + '2022-08-03' ) def test_deserialize_valid_date(self) -> None: self.assertEqual( - Iso8601Date.deserialize(o="2022-08-03"), + Iso8601Date.deserialize(o='2022-08-03'), date(year=2022, month=8, day=3) ) def test_deserialize_valid(self) -> None: with self.assertRaises(ValueError): - Iso8601Date.deserialize(o="2022-08-03zzz"), + Iso8601Date.deserialize(o='2022-08-03zzz') class TestXsdDate(TestCase): @@ -55,34 +55,34 @@ class TestXsdDate(TestCase): def test_deserialize_valid_1(self) -> None: self.assertEqual( - XsdDate.deserialize(o="2001-10-26"), + XsdDate.deserialize(o='2001-10-26'), date(year=2001, month=10, day=26) ) def test_deserialize_valid_2(self) -> None: with self.assertWarns(UserWarning): self.assertEqual( - XsdDate.deserialize(o="2001-10-26+02:00"), + XsdDate.deserialize(o='2001-10-26+02:00'), date(year=2001, month=10, day=26) ) def test_deserialize_valid_3(self) -> None: with self.assertWarns(UserWarning): self.assertEqual( - XsdDate.deserialize(o="2001-10-26Z"), + XsdDate.deserialize(o='2001-10-26Z'), date(year=2001, month=10, day=26) ) def test_deserialize_valid_4(self) -> None: with self.assertWarns(UserWarning): self.assertEqual( - XsdDate.deserialize(o="2001-10-26+00:00"), + XsdDate.deserialize(o='2001-10-26+00:00'), date(year=2001, month=10, day=26) ) def test_deserialize_valid_5(self) -> None: self.assertEqual( - XsdDate.deserialize(o="-2001-10-26"), + XsdDate.deserialize(o='-2001-10-26'), date(year=2001, month=10, day=26) ) @@ -100,13 +100,13 @@ class TestXsdDateTime(TestCase): def test_deserialize_valid_1(self) -> None: self.assertEqual( - XsdDateTime.deserialize(o="2001-10-26T21:32:52"), + XsdDateTime.deserialize(o='2001-10-26T21:32:52'), datetime(year=2001, month=10, day=26, hour=21, minute=32, second=52, tzinfo=None) ) def test_deserialize_valid_2(self) -> None: self.assertEqual( - XsdDateTime.deserialize(o="2001-10-26T21:32:52+02:00"), + XsdDateTime.deserialize(o='2001-10-26T21:32:52+02:00'), datetime( year=2001, month=10, day=26, hour=21, minute=32, second=52, tzinfo=timezone(timedelta(seconds=7200)) @@ -115,19 +115,19 @@ def test_deserialize_valid_2(self) -> None: def test_deserialize_valid_3(self) -> None: self.assertEqual( - XsdDateTime.deserialize(o="2001-10-26T19:32:52Z"), + XsdDateTime.deserialize(o='2001-10-26T19:32:52Z'), datetime(year=2001, month=10, day=26, hour=19, minute=32, second=52, tzinfo=timezone.utc) ) def test_deserialize_valid_4(self) -> None: self.assertEqual( - XsdDateTime.deserialize(o="2001-10-26T19:32:52+00:00"), + XsdDateTime.deserialize(o='2001-10-26T19:32:52+00:00'), datetime(year=2001, month=10, day=26, hour=19, minute=32, second=52, tzinfo=timezone.utc) ) def test_deserialize_valid_5(self) -> None: self.assertEqual( - XsdDateTime.deserialize(o="-2001-10-26T21:32:52"), + XsdDateTime.deserialize(o='-2001-10-26T21:32:52'), datetime(year=2001, month=10, day=26, hour=21, minute=32, second=52, tzinfo=None) )