diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a561cf..4ec0610 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,13 +1,15 @@ name: Build on: + release: + types: [published] push: branches: - master - ci pull_request: branches: - - '*' + - "*" env: PROJECT_NAME: essentials @@ -17,56 +19,83 @@ jobs: runs-on: ubuntu-18.04 strategy: matrix: - python-version: [3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] steps: - - uses: actions/checkout@v1 - with: - fetch-depth: 9 - submodules: false + - uses: actions/checkout@v1 + with: + fetch-depth: 9 + submodules: false - - name: Use Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} + - name: Use Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} - - uses: actions/cache@v1 - id: depcache - with: - path: deps - key: requirements-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }} + - uses: actions/cache@v1 + id: depcache + with: + path: deps + key: requirements-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }} - - name: Download dependencies - if: steps.depcache.outputs.cache-hit != 'true' - run: | - pip download --dest=deps -r requirements.txt + - name: Download dependencies + if: steps.depcache.outputs.cache-hit != 'true' + run: | + pip download --dest=deps -r requirements.txt - - name: Install dependencies - run: | - pip install -U --no-index --find-links=deps deps/* + - name: Install dependencies + run: | + pip install -U --no-index --find-links=deps deps/* - - name: Run tests - run: | - flake8 && pytest --doctest-modules --junitxml=junit/pytest-results-${{ matrix.python-version }}.xml --cov=$PROJECT_NAME --cov-report=xml tests/ + - name: Run tests + run: | + flake8 && pytest --doctest-modules --junitxml=junit/pytest-results-${{ matrix.python-version }}.xml --cov=$PROJECT_NAME --cov-report=xml tests/ - - name: Upload pytest test results - uses: actions/upload-artifact@master - with: - name: pytest-results-${{ matrix.python-version }} - path: junit/pytest-results-${{ matrix.python-version }}.xml - if: always() + - name: Upload pytest test results + uses: actions/upload-artifact@master + with: + name: pytest-results-${{ matrix.python-version }} + path: junit/pytest-results-${{ matrix.python-version }}.xml + if: always() - - name: Install distribution dependencies - run: pip install --upgrade twine setuptools wheel - if: matrix.python-version == 3.8 + - name: Codecov + run: | + bash <(curl -s https://codecov.io/bash) - - name: Create distribution package - run: python setup.py sdist bdist_wheel - if: matrix.python-version == 3.8 + - name: Install distribution dependencies + run: pip install --upgrade twine setuptools wheel + if: matrix.python-version == 3.8 || matrix.python-version == 3.9 - - name: Upload distribution package - uses: actions/upload-artifact@master - with: - name: dist-package-${{ matrix.python-version }} - path: dist - if: matrix.python-version == 3.8 + - name: Create distribution package + run: python setup.py sdist bdist_wheel + if: matrix.python-version == 3.8 || matrix.python-version == 3.9 + + - name: Upload distribution package + uses: actions/upload-artifact@master + with: + name: dist-package-${{ matrix.python-version }} + path: dist + if: matrix.python-version == 3.8 || matrix.python-version == 3.9 + + publish: + runs-on: ubuntu-18.04 + needs: build + if: github.event_name == 'release' + steps: + - name: Download a distribution artifact + uses: actions/download-artifact@v2 + with: + name: dist-package-3.9 + path: dist + - name: Publish distribution 📦 to Test PyPI + uses: pypa/gh-action-pypi-publish@master + with: + skip_existing: true + user: __token__ + password: ${{ secrets.test_pypi_password }} + repository_url: https://test.pypi.org/legacy/ + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.pypi_password }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a97e43e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.4] - 2020-11-08 :octocat: +- Completely migrates to GitHub Workflows +- Improves build to test Python 3.6 and 3.9 +- Improves the `json.FriendlyEncoder` class to handle built-in `dataclasses` +- Adds a changelog +- Improves badges diff --git a/README.md b/README.md index ca6616b..622cc29 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -[![Build Status](https://dev.azure.com/robertoprevato/Nest/_apis/build/status/RobertoPrevato.essentials?branchName=master)](https://dev.azure.com/robertoprevato/Nest/_build/latest?definitionId=28&branchName=master) [![pypi](https://img.shields.io/pypi/v/essentials.svg?color=blue)](https://pypi.org/project/essentials/) [![Test coverage](https://img.shields.io/azure-devops/coverage/robertoprevato/Nest/28.svg)](https://dev.azure.com/robertoprevato/Nest/_build?definitionId=28) +![Build](https://github.com/RobertoPrevato/essentials/workflows/Build/badge.svg) +[![pypi](https://img.shields.io/pypi/v/essentials.svg)](https://pypi.python.org/pypi/essentials) +[![versions](https://img.shields.io/pypi/pyversions/essentials.svg)](https://github.com/RobertoPrevato/essentials) +[![license](https://img.shields.io/github/license/RobertoPrevato/essentials.svg)](https://github.com/RobertoPrevato/essentials/blob/master/LICENSE) # Essentials Core classes and functions, reusable in any kind of Python application. diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 5457464..0000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,47 +0,0 @@ -# Python package -# Create and test a Python package on multiple Python versions. -# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more: -# https://docs.microsoft.com/azure/devops/pipelines/languages/python - -trigger: -- master - -pool: - vmImage: 'ubuntu-latest' -strategy: - matrix: - Python37: - python.version: '3.7' - Python38: - python.version: '3.8' - -steps: -- task: UsePythonVersion@0 - inputs: - versionSpec: '$(python.version)' - displayName: 'Use Python $(python.version)' - -- script: | - python -m pip install --upgrade pip - pip install -r requirements.txt - displayName: 'Install dependencies' - -- script: flake8 - displayName: Flake8 test - -- script: | - pip install pytest pytest-asyncio pytest-cov - pip install https://github.com/RobertoPrevato/pytest-azurepipelines/archive/css_styles.zip - python -m pytest tests/ --cov essentials --cov-report html - displayName: Run pytest tests - -- script: | - pip install --upgrade twine setuptools wheel - python setup.py sdist bdist_wheel - displayName: 'create artifacts' - -- task: PublishBuildArtifacts@1 - inputs: - pathtoPublish: dist - artifactName: 'dist_$(python.version)' - displayName: 'publish artifacts' diff --git a/essentials/json.py b/essentials/json.py index a535bc4..6bc8faa 100644 --- a/essentials/json.py +++ b/essentials/json.py @@ -2,25 +2,21 @@ This module defines a user-friendly json encoder, supporting time objects, UUID and bytes. """ -import json import base64 +import dataclasses +import json +from datetime import date, datetime, time from enum import Enum -from datetime import time, date, datetime from uuid import UUID - __all__ = ["FriendlyEncoder", "dumps"] -# TODO: use singledispatch to make the friendly encoder expandable - - class FriendlyEncoder(json.JSONEncoder): def default(self, obj): try: return json.JSONEncoder.default(self, obj) except TypeError: - if hasattr(obj, "dict"): return obj.dict() if isinstance(obj, time): @@ -35,6 +31,8 @@ def default(self, obj): return str(obj) if isinstance(obj, Enum): return obj.value + if dataclasses.is_dataclass(obj): + return dataclasses.asdict(obj) raise diff --git a/requirements.txt b/requirements.txt index 175c64d..65ffdc5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,4 @@ pytest pytest-cov pytest-asyncio flake8 -mypy -black +dataclasses==0.7;python_version<'3.7' diff --git a/setup.py b/setup.py index 685732e..6d1961e 100644 --- a/setup.py +++ b/setup.py @@ -8,15 +8,19 @@ def readme(): setup( name="essentials", - version="1.1.3", + version="1.1.4", description="General purpose classes and functions, " - "reusable in any kind of Python application", + "reusable in any kind of Python application", long_description=readme(), long_description_content_type="text/markdown", classifiers=[ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Operating System :: OS Independent", ], url="https://github.com/RobertoPrevato/essentials", @@ -24,11 +28,7 @@ def readme(): author_email="roberto.prevato@gmail.com", keywords="core utilities", license="MIT", - packages=[ - "essentials", - "essentials.typesutils", - "essentials.decorators", - ], - install_requires=[], + packages=["essentials", "essentials.typesutils", "essentials.decorators"], + install_requires=["dataclasses==0.7;python_version<'3.7'"], include_package_data=True, ) diff --git a/tests/test_json.py b/tests/test_json.py index 284a6ba..8741157 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -1,4 +1,5 @@ -import uuid +from uuid import UUID, uuid4 +from dataclasses import dataclass from datetime import date, datetime, time from enum import Enum, Flag, IntEnum, IntFlag, auto @@ -8,8 +9,13 @@ from essentials.json import dumps -class Fruit(Enum): +@dataclass +class Foo: + id: UUID + name: str + +class Fruit(Enum): ANANAS = "ananas" BANANA = "banana" MANGO = "mango" @@ -42,7 +48,7 @@ class Permission(IntFlag): ({"value": date(2016, 3, 26)}, '{"value": "2016-03-26"}'), ({"value": datetime(2016, 3, 26, 3, 0, 0)}, '{"value": "2016-03-26T03:00:00"}'), ( - {"value": uuid.UUID("e56fddfc-f85b-4178-869f-a218278a639e")}, + {"value": UUID("e56fddfc-f85b-4178-869f-a218278a639e")}, '{"value": "e56fddfc-f85b-4178-869f-a218278a639e"}', ), ( @@ -85,7 +91,6 @@ def __init__(self, x, y): def test_enum_to_json(): - value = dumps( { "fruit": Fruit.MANGO, @@ -98,7 +103,6 @@ def test_enum_to_json(): def test_int_enum_to_json(): - value = dumps({"power": Power.GREAT, "powers": [Power.MILD, Power.MODERATE]}) assert '"power": 3' in value @@ -106,7 +110,6 @@ def test_int_enum_to_json(): def test_intflag_enum_to_json(): - value = dumps( { "permission_one": Permission.R, @@ -119,7 +122,6 @@ def test_intflag_enum_to_json(): def test_flag_enum_to_json(): - value = dumps( { "color_one": Color.GREEN, @@ -129,3 +131,17 @@ def test_flag_enum_to_json(): ) assert '{"color_one": 4, "color_two": 5, "color_three": 7}' == value + + +def test_serialize_dataclass(): + foo_id = uuid4() + value = dumps(Foo(foo_id, "foo")) + + assert f'{{"id": "{foo_id}", "name": "foo"}}' == value + + +def test_serialize_dataclass_no_spaces(): + foo_id = uuid4() + value = dumps(Foo(foo_id, "foo"), separators=(",", ":")) + + assert f'{{"id":"{foo_id}","name":"foo"}}' == value diff --git a/tests/test_registry.py b/tests/test_registry.py index 6c9b8c4..87b8cdf 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -1,8 +1,7 @@ from pytest import raises from essentials.exceptions import InvalidArgument -from essentials.registry import (AmbiguousRegistryName, Registry, - TypeNotFoundException) +from essentials.registry import AmbiguousRegistryName, Registry, TypeNotFoundException def test_registry_type(): diff --git a/tests/test_time_utils.py b/tests/test_time_utils.py index 4f7b58f..273a8be 100644 --- a/tests/test_time_utils.py +++ b/tests/test_time_utils.py @@ -2,11 +2,14 @@ import pytest -from essentials.typesutils.timeutils import (TimePrecision, get_time_precision, - time_from_microseconds, - time_from_seconds, - time_to_microseconds, - time_to_seconds) +from essentials.typesutils.timeutils import ( + TimePrecision, + get_time_precision, + time_from_microseconds, + time_from_seconds, + time_to_microseconds, + time_to_seconds, +) @pytest.mark.parametrize( @@ -38,10 +41,7 @@ def test_time_from_seconds(seconds, expected_time): @pytest.mark.parametrize( "expected_time,microseconds", - [ - [time(0, 1, 0, 20), 60 * 1e6 + 20], - [time(0, 5, 0, 333), 60 * 5 * 1e6 + 333] - ], + [[time(0, 1, 0, 20), 60 * 1e6 + 20], [time(0, 5, 0, 333), 60 * 5 * 1e6 + 333]], ) def test_time_from_microseconds(microseconds, expected_time): assert time_from_microseconds(microseconds) == expected_time @@ -49,10 +49,7 @@ def test_time_from_microseconds(microseconds, expected_time): @pytest.mark.parametrize( "value,expected_microseconds", - [ - [time(0, 1, 0, 20), 60 * 1e6 + 20], - [time(0, 5, 0, 333), 60 * 5 * 1e6 + 333] - ], + [[time(0, 1, 0, 20), 60 * 1e6 + 20], [time(0, 5, 0, 333), 60 * 5 * 1e6 + 333]], ) def test_time_to_microseconds(value, expected_microseconds): assert time_to_microseconds(value) == expected_microseconds