From 2cee4d5f48d59a737f4fc7b0e3d26fbce33c2392 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 22 Jan 2024 12:51:38 +0100 Subject: [PATCH] feat!: v1.0.0 (#55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Release of first major version 🎉** ## BREAKING Changes * Dropped support for python <3.8 --------- Signed-off-by: Jan Kowalleck --- .editorconfig | 37 ++++++ .flake8 | 21 ++++ .../workflows/manual-release-candidate.yml | 82 ------------- .github/workflows/python.yml | 48 ++------ .github/workflows/release.yml | 110 +++++++++++++++--- .pytype.toml | 2 +- CONTRIBUTING.md | 2 +- docs/conf.py | 3 +- pyproject.toml | 66 +++++++---- requirements.lowest.txt | 23 ---- serializable/__init__.py | 86 ++++++-------- serializable/_logging.py | 34 ------ serializable/formatters.py | 10 +- serializable/helpers.py | 13 ++- tests/test_helpers.py | 6 +- tests/test_xml.py | 3 - tox.ini | 32 ++--- 17 files changed, 265 insertions(+), 313 deletions(-) create mode 100644 .editorconfig create mode 100644 .flake8 delete mode 100644 .github/workflows/manual-release-candidate.yml delete mode 100644 requirements.lowest.txt delete mode 100644 serializable/_logging.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0b244df --- /dev/null +++ b/.editorconfig @@ -0,0 +1,37 @@ +# EditorConfig is awesome: https://EditorConfig.org + +root = true + +[*] +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +end_of_line = lf + +[*.py] +indent_style = space +indent_size = 4 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.toml] +indent_style = space +indent_size = 2 + +[*.md] +charset = latin1 +indent_style = space +indent_size = 2 +# 2 trailing spaces indicate line breaks. +trim_trailing_whitespace = false + +[*.{rst,txt}] +indent_style = space +indent_size = 4 + +[{*.ini,.bandit,.flake8}] +charset = latin1 +indent_style = space +indent_size = 4 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..f5ab325 --- /dev/null +++ b/.flake8 @@ -0,0 +1,21 @@ +[flake8] +## https://flake8.pycqa.org/en/latest/user/configuration.html +## keep in sync with isort config - in `isort.cfg` file + +exclude = + build,dist,__pycache__,.eggs,*.egg-info*, + *_cache,*.cache, + .git,.tox,.venv,venv + _OLD,_TEST, + docs + +max-line-length = 120 + +# max-complexity = 10 + +ignore = + # ignore `self`, `cls` markers of flake8-annotations>=2.0 + ANN101,ANN102 + # ignore Opinionated Warnings - which are documented as disabled by default + # See https://github.com/sco1/flake8-annotations#opinionated-warnings + ANN401 \ No newline at end of file diff --git a/.github/workflows/manual-release-candidate.yml b/.github/workflows/manual-release-candidate.yml deleted file mode 100644 index 5c7cfe8..0000000 --- a/.github/workflows/manual-release-candidate.yml +++ /dev/null @@ -1,82 +0,0 @@ -# encoding: utf-8 - -# This file is part of py-serializable -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) Paul Horton. All Rights Reserved. - -name: Manual Pre Release Publish - -on: - workflow_dispatch: - -concurrency: - group: release - cancel-in-progress: true - -env: - REPORTS_DIR: CI_reports - DIST_DIR: dist - DIST_ARTIFACT: python-dist - PYTHON_SEMANTIC_RELEASE_VERSION: "7.34.6" - PYTHON_VERSION_DEFAULT: "3.11" - POETRY_VERSION: "1.7.1" - -jobs: - release_candidate: - runs-on: ubuntu-latest - steps: - - name: Checkout - # see https://github.com/actions/checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Python Environment - # see https://github.com/actions/setup-python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - architecture: 'x64' - - - name: Install poetry - # see https://github.com/marketplace/actions/setup-poetry - uses: Gr1N/setup-poetry@v8 - with: - poetry-version: ${{ env.POETRY_VERSION }} - - - name: Install dependencies - run: poetry install --no-root - - - name: View poetry version - run: poetry --version - - - name: Install semantic-release - run: pip install python-semantic-release==${{ env.PYTHON_SEMANTIC_RELEASE_VERSION }} - - - name: Perform Release 📦 - run: semantic-release publish --prerelease - env: - GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} - REPOSITORY_USERNAME: __token__ - REPOSITORY_PASSWORD: ${{ secrets.PYPI_TOKEN }} - - - name: Artifact python dist - # see https://github.com/actions/upload-artifact - uses: actions/upload-artifact@v3 - with: - name: ${{ env.DIST_ARTIFACT }} - path: ${{ env.DIST_DIR }}/ - if-no-files-found: error diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index c205e66..270be4e 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -49,25 +49,21 @@ jobs: - name: Checkout # see https://github.com/actions/checkout uses: actions/checkout@v4 - - name: Setup Python Environment # see https://github.com/actions/setup-python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION_DEFAULT }} architecture: 'x64' - - name: Install poetry # see https://github.com/marketplace/actions/setup-poetry uses: Gr1N/setup-poetry@v8 with: poetry-version: ${{ env.POETRY_VERSION }} - - name: Install dependencies run: poetry install - - name: Run tox - run: poetry run tox -e flake8 -s false + run: poetry run tox run -e flake8 -s false static-code-analysis: name: Static Coding Analysis (py${{ matrix.python-version}} ${{ matrix.toxenv-factor }}) @@ -77,39 +73,34 @@ jobs: fail-fast: false matrix: include: - - # test with the locked dependencies + - # latest python python-version: '3.12' - toxenv-factor: 'locked' - - # test with the lowest dependencies - python-version: '3.7' + toxenv-factor: 'current' + - # lowest supported python + python-version: '3.8' toxenv-factor: 'lowest' - poetry-version: '1.4.1' # py3.7 does not allow latest poetry steps: - name: Checkout # see https://github.com/actions/checkout uses: actions/checkout@v4 - - name: Setup Python Environment # see https://github.com/actions/setup-python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} architecture: 'x64' - - name: Install poetry # see https://github.com/marketplace/actions/setup-poetry uses: Gr1N/setup-poetry@v8 with: - poetry-version: ${{ matrix.poetry-version || env.POETRY_VERSION }} - + poetry-version: ${{ env.POETRY_VERSION }} - name: Install Dependencies run: poetry install --no-interaction --no-root - - name: Run tox - run: poetry run tox -re mypy-${{ matrix.toxenv-factor }} -s false + run: poetry run tox run -r -e mypy-${{ matrix.toxenv-factor }} -s false build-and-test: - name: Test (${{ matrix.os }} py${{ matrix.python-version }} ${{ matrix.toxenv-factor }}) + name: Test (${{ matrix.os }} py${{ matrix.python-version }}) runs-on: ${{ matrix.os }} timeout-minutes: 10 env: @@ -123,54 +114,35 @@ jobs: - "3.11" - "3.10" - "3.9" - - "3.8" - - "3.7" # lowest supported - toxenv-factor: ['locked'] - include: - - # test with the lowest dependencies - os: 'ubuntu-latest' - python-version: '3.7' - toxenv-factor: 'lowest' + - "3.8" # lowest supported steps: - name: Checkout # see https://github.com/actions/checkout uses: actions/checkout@v4 - - name: Create reports directory run: mkdir ${{ env.REPORTS_DIR }} - - name: Setup Python Environment # see https://github.com/actions/setup-python uses: actions/setup-python@v5 with: 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 with: poetry-version: ${{ env.POETRY_VERSION }} - - name: Install dependencies run: poetry install - - name: Ensure build successful run: poetry build - - name: Run tox - run: poetry run tox -v -r -e py-${{ matrix.toxenv-factor }} -s false - + run: poetry run tox run -v -r -e py -s false - name: Generate coverage reports run: > poetry run coverage report && poetry run coverage xml -o ${{ env.REPORTS_DIR }}/coverage-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.toxenv-factor }}.xml && poetry run coverage html -d ${{ env.REPORTS_DIR }} - - name: Artifact reports if: ${{ ! cancelled() }} # see https://github.com/actions/upload-artifact diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54b3ab2..5f440be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,23 +21,82 @@ name: Release on: push: - branches: [ 'main' ] + branches: [ 'main', 'master' ] workflow_dispatch: + inputs: + release_force: + # see https://python-semantic-release.readthedocs.io/en/latest/github-action.html#command-line-options + description: | + Force release be one of: [major | minor | patch] + Leave empty for auto-detect based on commit messages. + type: choice + options: + - "" # auto - no force + - major # force major + - minor # force minor + - patch # force patch + default: "" + required: false + prerelease_token: + description: 'The "prerelease identifier" to use as a prefix for the "prerelease" part of a semver. Like the rc in `1.2.0-rc.8`.' + type: choice + options: + - rc + - beta + - alpha + default: rc + required: false + prerelease: + description: "Is a pre-release" + type: boolean + default: false + required: false concurrency: - group: release - cancel-in-progress: true + group: deploy + cancel-in-progress: false # prevent hickups with semantic-release env: PYTHON_VERSION_DEFAULT: "3.11" POETRY_VERSION: "1.7.1" jobs: + quicktest: + runs-on: ubuntu-latest + steps: + - name: Checkout code + # see https://github.com/actions/checkout + uses: actions/checkout@v4 + - name: Setup Python Environment + # see https://github.com/actions/setup-python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION_DEFAULT }} + architecture: 'x64' + - name: Install poetry + # see https://github.com/marketplace/actions/setup-poetry + uses: Gr1N/setup-poetry@v8 + with: + poetry-version: ${{ env.POETRY_VERSION }} + - name: Install dependencies + run: poetry install --no-root + - name: Run tox + run: poetry run tox run -e py -s false + release: - # https://github.community/t/how-do-i-specify-job-dependency-running-in-another-workflow/16482 + needs: + - quicktest + # https://github.community/t/how-do-i-specify-job-dependency-running-in-another-workflow/16482 # limit this to being run on regular commits, not the commits that semantic-release will create - if: github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, 'chore(release):') + # but also allow manual workflow dispatch + if: "!contains(github.event.head_commit.message, 'chore(release):')" runs-on: ubuntu-latest + permissions: + # NOTE: this enables trusted publishing. + # See https://github.com/pypa/gh-action-pypi-publish/tree/release/v1#trusted-publishing + # and https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ + id-token: write + contents: write steps: - name: Checkout # see https://github.com/actions/checkout @@ -51,25 +110,42 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION_DEFAULT }} architecture: 'x64' - - - name: Install poetry - # see https://github.com/marketplace/actions/setup-poetry - uses: Gr1N/setup-poetry@v8 + - name: Install and configure Poetry + # See https://github.com/marketplace/actions/install-poetry-action + uses: snok/install-poetry@v1 with: - poetry-version: ${{ env.POETRY_VERSION }} - + version: ${{ env.POETRY_VERSION }} + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true - name: Install dependencies run: poetry install --no-root - - name: View poetry version run: poetry --version - name: Python Semantic Release + id: release # see https://python-semantic-release.readthedocs.io/en/latest/automatic-releases/github-actions.html # see https://github.com/python-semantic-release/python-semantic-release - uses: python-semantic-release/python-semantic-release@v7.34.6 + uses: python-semantic-release/python-semantic-release@v8.5.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + force: ${{ github.event.inputs.release_force }} + prerelease: ${{ github.event.inputs.prerelease }} + prerelease_token: ${{ github.event.inputs.prerelease_token }} + + - name: Publish package distributions to PyPI + if: steps.release.outputs.released == 'true' + # see https://github.com/pypa/gh-action-pypi-publish + uses: pypa/gh-action-pypi-publish@release/v1 with: - github_token: ${{ secrets.GH_RELEASE_TOKEN }} - repository_username: __token__ - repository_password: ${{ secrets.PYPI_TOKEN }} - pypi_token: ${{ secrets.PYPI_TOKEN }} + password: ${{ secrets.PYPI_TOKEN }} + + - name: Publish package distributions to GitHub Releases + if: steps.release.outputs.released == 'true' + # see https://github.com/python-semantic-release/upload-to-gh-release + uses: python-semantic-release/upload-to-gh-release@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ steps.release.outputs.tag }} + diff --git a/.pytype.toml b/.pytype.toml index 6d68ea2..cba3250 100644 --- a/.pytype.toml +++ b/.pytype.toml @@ -35,7 +35,7 @@ output = '.pytype' pythonpath = '.' # Python version (major.minor) of the target code. -# python_version = '3.7' +# python_version = '3.8' # Enable parameter count checks for overriding methods. This flag is temporary # and will be removed once this behavior is enabled by default. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52ef846..4ce7129 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ 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 +Even though this library is designed to be runnable on python>=3.8.0 some development-tools require python>=3.8.1 To install dev-dependencies and tools: diff --git a/docs/conf.py b/docs/conf.py index 0df8f55..3eb5a09 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,6 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. -import pkg_resources # -- Project information ----------------------------------------------------- @@ -25,7 +24,7 @@ # The full version, including alpha/beta/rc tags # !! version is managed by semantic_release -release = '0.17.1' +release = "1.0.0-rc.1" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 7e29448..307a75d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "py-serializable" -version = "0.17.1" +version = "1.0.0-rc.1" description = "Library for serializing and deserializing Python Objects to and from JSON and XML." authors = ["Paul Horton "] maintainers = [ @@ -27,7 +27,6 @@ classifiers = [ 'Intended Audience :: Information Technology', 'Topic :: Software Development', 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', @@ -43,39 +42,58 @@ keywords = [ "Bug Tracker" = "https://github.com/madpah/serializable/issues" [tool.poetry.dependencies] -python = "^3.7" +python = "^3.8" defusedxml = "^0.7.1" [tool.poetry.dev-dependencies] -typing-extensions = {version = "4.7.1", python = "<3.8"} -tox = "3.28.0" -coverage = "7.2.7" +tox = "4.11.4" +coverage = "7.4.0" 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"} +mypy = "1.8.0" +autopep8 = "2.0.4" +isort = "5.13.2" flake8 = { version="7.0.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"} -flake8-logging = {version = "1.4.0", python = ">=3.8"} +flake8-isort = "6.1.1" +flake8-quotes = "3.3.2" +flake8-use-fstring = "1.4" +flake8-logging = "1.4.0" + [tool.semantic_release] # see https://python-semantic-release.readthedocs.io/en/latest/configuration.html -# currently running in CI: v7.34.6 -commit_message = "chore(release): {version}\n\nAutomatically generated by python-semantic-release" -version_toml = "pyproject.toml:tool.poetry.version" -version_variable = [ +commit_author = "semantic-release " +commit_message = "chore(release): {version}\n\nAutomatically generated by python-semantic-release\n\nSigned-off-by: semantic-release " +upload_to_vcs_release = true +build_command = "pip install poetry && poetry build" +version_toml = ["pyproject.toml:tool.poetry.version"] +version_variables = [ "serializable/__init__.py:__version__", "docs/conf.py:release", ] -branch = "main" -upload_to_pypi = true -upload_to_release = true -build_command = "pip install poetry==1.1.12 && poetry build" \ No newline at end of file + +[tool.semantic_release.publish] +dist_glob_patterns = ["dist/*"] +upload_to_vcs_release = true + +[tool.semantic_release.changelog] +changelog_file = "CHANGELOG.md" +exclude_commit_patterns = [ + "chore\\(release\\):", +] + +[tool.semantic_release.branches.main] +match = "(main|master)" +prerelease = false + +[tool.semantic_release.branches."step"] +match = "(build|chore|ci|docs|feat|fix|perf|style|refactor|test)" +prerelease = true +prerelease_token = "alpha" + +[tool.semantic_release.branches."major-dev"] +match = "(\\d+\\.0\\.0-(dev|rc)|dev/\\d+\\.0\\.0)" +prerelease = true +prerelease_token = "rc" diff --git a/requirements.lowest.txt b/requirements.lowest.txt deleted file mode 100644 index 74507c8..0000000 --- a/requirements.lowest.txt +++ /dev/null @@ -1,23 +0,0 @@ -# encoding: utf-8 - -# This file is part of py-serializable -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) Paul Horton. All Rights Reserved. - -# Exactly pinned dependencies to the lowest version regardless of python_version -# See pyptoject file for ranges - -defusedxml == 0.7.1 \ No newline at end of file diff --git a/serializable/__init__.py b/serializable/__init__.py index 3126eb3..ac8981d 100644 --- a/serializable/__init__.py +++ b/serializable/__init__.py @@ -17,22 +17,23 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. -import enum -import inspect -import json -import re from copy import copy from decimal import Decimal +from enum import Enum, EnumMeta, unique +from inspect import getfullargspec, getmembers, isclass from io import StringIO, TextIOBase -from json import JSONEncoder +from json import JSONEncoder, dumps as json_dumps +from logging import NullHandler, getLogger +from re import compile as re_compile, search as re_search from typing import ( - TYPE_CHECKING, Any, Callable, Dict, Iterable, List, + Literal, Optional, + Protocol, Set, Tuple, Type, @@ -43,24 +44,11 @@ ) from xml.etree.ElementTree import Element, SubElement -from defusedxml import ElementTree as SafeElementTree # type: ignore +from defusedxml import ElementTree as SafeElementTree # type:ignore[import-untyped] -from ._logging import _logger, _warning_kwargs from .formatters import BaseNameFormatter, CurrentFormatter from .helpers import BaseHelper -if TYPE_CHECKING: # pragma: no cover - 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 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/ # see https://github.com/python/typing/issues/213 @@ -71,12 +59,14 @@ # !! version is managed by semantic_release # do not use typing here, or else `semantic_release` might have issues finding the variable -__version__ = '0.17.1' +__version__ = '1.0.0-rc.1' -# make logger publicly available, as stable API +_logger = getLogger(__name__) +_logger.addHandler(NullHandler()) +# make `logger` publicly available, as stable API logger = _logger """ -The logger. The thing that captures all this module has to say. +The logger. The thing that captures all this package has to say. Feel free to modify its level and attach handlers to it. """ @@ -88,11 +78,11 @@ class ViewType: _F = TypeVar('_F', bound=Callable[..., Any]) _T = TypeVar('_T') -_E = TypeVar('_E', bound=enum.Enum) +_E = TypeVar('_E', bound=Enum) -@enum.unique -class SerializationType(str, enum.Enum): +@unique +class SerializationType(str, Enum): """ Enum to define the different formats supported for serialization and deserialization. """ @@ -107,8 +97,8 @@ class SerializationType(str, enum.Enum): ) -@enum.unique -class XmlArraySerializationType(enum.Enum): +@unique +class XmlArraySerializationType(Enum): """ Enum to differentiate how array-type properties (think Iterables) are serialized. @@ -187,7 +177,7 @@ def view(self) -> Optional[Type[ViewType]]: def default(self, o: Any) -> Any: # Enum - if isinstance(o, enum.Enum): + if isinstance(o, Enum): return o.value # Iterables @@ -264,7 +254,7 @@ def as_json(self: Any, view_: Optional[Type[ViewType]] = None) -> str: ``serializable``. """ _logger.debug('Dumping %s to JSON with view: %s...', self, view_) - return json.dumps(self, cls=_SerializableJsonEncoder, view_=view_) + return json_dumps(self, cls=_SerializableJsonEncoder, view_=view_) @classmethod def from_json(cls: Type[_T], data: Dict[str, Any]) -> Optional[_T]: @@ -280,7 +270,7 @@ def from_json(cls: Type[_T], data: Dict[str, Any]) -> Optional[_T]: if klass is None: _logger.warning( '%s is not a known serializable class', klass_qualified_name, - **_warning_kwargs) # type:ignore[arg-type] + stacklevel=2) return None if len(klass_properties) == 1: @@ -346,11 +336,10 @@ def from_json(cls: Type[_T], data: Dict[str, Any]) -> Optional[_T]: v = str(v) _data[k] = prop_info.concrete_type(v) except AttributeError as e: - _logger.error('There was an AttributeError deserializing JSON to %s.\n' - 'The Property is: %s\n' - 'The Value was: %s\n' - 'Exception: %s\n', - cls, prop_info, v, e) + _logger.exception('There was an AttributeError deserializing JSON to %s.\n' + 'The Property is: %s\n' + 'The Value was: %s\n', + cls, prop_info, v) raise AttributeError( f'There was an AttributeError deserializing JSON to {cls} the Property {prop_info}: {e}' ) from e @@ -510,7 +499,7 @@ def from_xml(cls: Type[_T], data: Union[TextIOBase, Element], klass = ObjectMetadataLibrary.klass_mappings.get(f'{cls.__module__}.{cls.__qualname__}', None) if klass is None: _logger.warning('%s.%s is not a known serializable class', cls.__module__, cls.__qualname__, - **_warning_kwargs) # type:ignore[arg-type] + stacklevel=2) return None klass_properties = ObjectMetadataLibrary.klass_property_mappings.get(f'{cls.__module__}.{cls.__qualname__}', {}) @@ -522,7 +511,7 @@ def from_xml(cls: Type[_T], data: Union[TextIOBase, Element], _namespaces = dict([node for _, node in SafeElementTree.iterparse(StringIO(SafeElementTree.tostring(data, 'unicode')), events=['start-ns'])]) - default_namespace = (re.compile(r'^\{(.*?)\}.').search(data.tag) or (None, _namespaces.get('')))[1] + default_namespace = (re_compile(r'^\{(.*?)\}.').search(data.tag) or (None, _namespaces.get('')))[1] if default_namespace is None: def strip_default_namespace(s: str) -> str: @@ -643,11 +632,10 @@ def strip_default_namespace(s: str) -> str: else: _data[decoded_k] = prop_info.concrete_type(child_e.text) except AttributeError as e: - _logger.error('There was an AttributeError deserializing JSON to %s.\n' - 'The Property is: %s\n' - 'The Value was: %s\n' - 'Exception: %s\n', - cls, prop_info, v, e) + _logger.exception('There was an AttributeError deserializing JSON to %s.\n' + 'The Property is: %s\n' + 'The Value was: %s\n', + cls, prop_info, v) raise AttributeError( f'There was an AttributeError deserializing XML to {cls} the Property {prop_info}: {e}' ) from e @@ -684,7 +672,7 @@ class ObjectMetadataLibrary: _klass_property_types: Dict[str, type] = {} _klass_property_views: Dict[str, Set[Type[ViewType]]] = {} _klass_property_xml_sequence: Dict[str, int] = {} - custom_enum_klasses: Set[Type[enum.Enum]] = set() + custom_enum_klasses: Set[Type[Enum]] = set() klass_mappings: Dict[str, 'ObjectMetadataLibrary.SerializableClass'] = {} klass_property_mappings: Dict[str, Dict[str, 'ObjectMetadataLibrary.SerializableProperty']] = {} @@ -853,7 +841,7 @@ def get_none_value(self, view_: Optional[Type[ViewType]] = None) -> Any: def is_helper_type(self) -> bool: ct = self.custom_type - return inspect.isclass(ct) and issubclass(ct, BaseHelper) + return isclass(ct) and issubclass(ct, BaseHelper) def is_primitive_type(self) -> bool: return self.concrete_type in self._PRIMITIVE_TYPES @@ -874,7 +862,7 @@ def _parse_type(self, type_: Any) -> None: self._is_optional = True type_to_parse = type_to_parse[9:-1] - match = re.search(r"^(?P[\w.]+)\[['\"]?(?P\w+)['\"]?]$", type_to_parse) + match = re_search(r"^(?P[\w.]+)\[['\"]?(?P\w+)['\"]?]$", type_to_parse) if match: results = match.groupdict() if results.get('array_type', None) in self._SORTED_CONTAINERS_TYPES: @@ -962,7 +950,7 @@ def _parse_type(self, type_: Any) -> None: self._concrete_type = self.type_ # Handle Enums - if issubclass(type(self.concrete_type), enum.EnumMeta): + if issubclass(type(self.concrete_type), EnumMeta): self._is_enum = True # Ensure marked as not deferred @@ -1039,9 +1027,9 @@ def register_klass(cls, klass: Type[_T], custom_name: Optional[str], qualified_class_name = f'{klass.__module__}.{klass.__qualname__}' cls.klass_property_mappings.update({qualified_class_name: {}}) _logger.debug('Registering Class %s with custom name %s', qualified_class_name, custom_name) - for name, o in inspect.getmembers(klass, ObjectMetadataLibrary.is_property): + for name, o in getmembers(klass, ObjectMetadataLibrary.is_property): qualified_property_name = f'{qualified_class_name}.{name}' - prop_arg_specs = inspect.getfullargspec(o.fget) + prop_arg_specs = getfullargspec(o.fget) cls.klass_property_mappings[qualified_class_name].update({ name: ObjectMetadataLibrary.SerializableProperty( diff --git a/serializable/_logging.py b/serializable/_logging.py deleted file mode 100644 index 9fa99c9..0000000 --- a/serializable/_logging.py +++ /dev/null @@ -1,34 +0,0 @@ -# encoding: utf-8 - -# This file is part of py-serializable -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) Paul Horton. All Rights Reserved. - - -from logging import NullHandler, getLogger -from sys import version_info - -# Attention: logger's name is pseudo-public API! -# Use the package's dist-name here. -_logger = getLogger('serializable') - -# This handler does nothing. It's intended to be used to avoid the -# "No handlers could be found for logger XXX" one-off warning. This is -# important for library code, which may contain code to log events. -_logger.addHandler(NullHandler()) - -# `logger.warning()` got additional kwarg since py38 -_warning_kwargs = {'stacklevel': 2} if version_info >= (3, 8) else {} diff --git a/serializable/formatters.py b/serializable/formatters.py index 2925724..affba22 100644 --- a/serializable/formatters.py +++ b/serializable/formatters.py @@ -17,8 +17,8 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. -import re from abc import ABC, abstractmethod +from re import compile as re_compile from typing import Type @@ -49,8 +49,8 @@ def encode_handle_python_builtins_and_keywords(cls, name: str) -> str: class CamelCasePropertyNameFormatter(BaseNameFormatter): - _ENCODE_PATTERN = re.compile(r'_([a-z])') - _DECODE_PATTERN = re.compile(r'(? str: @@ -67,7 +67,7 @@ def decode(cls, property_name: str) -> str: class KebabCasePropertyNameFormatter(BaseNameFormatter): - _ENCODE_PATTERN = re.compile(r'(_)') + _ENCODE_PATTERN = re_compile(r'(_)') @classmethod def encode(cls, property_name: str) -> str: @@ -81,7 +81,7 @@ def decode(cls, property_name: str) -> str: class SnakeCasePropertyNameFormatter(BaseNameFormatter): - _ENCODE_PATTERN = re.compile(r'(.)([A-Z][a-z]+)') + _ENCODE_PATTERN = re_compile(r'(.)([A-Z][a-z]+)') @classmethod def encode(cls, property_name: str) -> str: diff --git a/serializable/helpers.py b/serializable/helpers.py index 437df80..d0e2721 100644 --- a/serializable/helpers.py +++ b/serializable/helpers.py @@ -17,12 +17,11 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. -import re from datetime import date, datetime +from logging import getLogger +from re import sub as re_sub from typing import TYPE_CHECKING, Any, Optional, Type, TypeVar, Union -from ._logging import _logger, _warning_kwargs - if TYPE_CHECKING: # pragma: no cover from xml.etree.ElementTree import Element @@ -30,6 +29,8 @@ _T = TypeVar('_T') +_logger = getLogger(__name__) + class BaseHelper: """Base Helper. @@ -162,12 +163,12 @@ def deserialize(cls, o: Any) -> date: o = str(o)[:-1] _logger.warning( 'Potential data loss will occur: dates with timezones not supported in Python', - **_warning_kwargs) # type:ignore[arg-type] + stacklevel=2) if '+' in str(o): o = str(o)[:str(o).index('+')] _logger.warning( 'Potential data loss will occur: dates with timezones not supported in Python', - **_warning_kwargs) # type:ignore[arg-type] + stacklevel=2) return date.fromisoformat(str(o)) except ValueError: raise ValueError(f'Date string supplied ({o}) is not a supported ISO Format') @@ -190,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/test_helpers.py b/tests/test_helpers.py index 0faa513..50b1576 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -67,7 +67,7 @@ def test_deserialize_valid_2(self) -> None: date(year=2001, month=10, day=26) ) self.assertIn( - 'WARNING:serializable:' + 'WARNING:serializable.helpers:' 'Potential data loss will occur: dates with timezones not supported in Python', logs.output) @@ -78,7 +78,7 @@ def test_deserialize_valid_3(self) -> None: date(year=2001, month=10, day=26) ) self.assertIn( - 'WARNING:serializable:' + 'WARNING:serializable.helpers:' 'Potential data loss will occur: dates with timezones not supported in Python', logs.output) @@ -89,7 +89,7 @@ def test_deserialize_valid_4(self) -> None: date(year=2001, month=10, day=26) ) self.assertIn( - 'WARNING:serializable:' + 'WARNING:serializable.helpers:' 'Potential data loss will occur: dates with timezones not supported in Python', logs.output) diff --git a/tests/test_xml.py b/tests/test_xml.py index 185ffa5..01f8f5e 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -20,8 +20,6 @@ import logging import os from copy import deepcopy -from sys import version_info -from unittest import skipIf from defusedxml import ElementTree as SafeElementTree @@ -96,7 +94,6 @@ def test_serializable_no_defaultNS(self) -> None: self.maxDiff = None self.assertEqual(expected, actual) - @skipIf(version_info < (3, 8), '`ElementTree.tostring(default_namespace=)` not available') def test_serializable_with_defaultNS(self) -> None: """regression test for https://github.com/madpah/serializable/issues/12""" from xml.etree import ElementTree diff --git a/tox.ini b/tox.ini index ad40e7a..49c1158 100644 --- a/tox.ini +++ b/tox.ini @@ -20,12 +20,11 @@ # To use it, "pip install tox" and then run "tox" from this directory. [tox] -minversion = 3.10 +minversion = 4.0 envlist = flake8 - mypy-{locked,lowest} - py{312,311,310,39,38,37}-{locked,lowest} -isolated_build = True + mypy-{current,lowest} + py{312,311,310,39,38} skip_missing_interpreters = True usedevelop = False download = False @@ -36,38 +35,21 @@ skip_install = true allowlist_externals = poetry commands_pre = {envpython} --version - poetry install --no-root -v - lowest: poetry run pip install -U -r requirements.lowest.txt + poetry install -v poetry run pip freeze commands = poetry run coverage run --source=serializable -m unittest -v -[testenv:mypy{,-locked,-lowest}] +[testenv:mypy{,-current,-lowest}] skip_install = True commands = # mypy config is on own file: `.mypy.ini` !lowest: poetry run mypy - lowest: poetry run mypy --python-version=3.7 + lowest: poetry run mypy --python-version=3.8 [testenv:flake8] skip_install = True commands = + # mypy config is in own file: `.flake8` poetry run flake8 serializable/ tests/ -[flake8] -## keep in sync with isort config - in `isort.cfg` file -exclude = - build,dist,__pycache__,.eggs,*.egg-info*, - *_cache,*.cache, - .git,.tox,.venv,venv - _OLD,_TEST, - docs -max-line-length = 120 -ignore = E305, I003 - # ignore `self`, `cls` markers of flake8-annotations>=2.0 - ANN101,ANN102 - # ignore Opinionated Warnings - which are documented as disabled by default - # See https://github.com/sco1/flake8-annotations#opinionated-warnings - ANN401 - # serializable/helpers.py:28 - B027