From 3b81d9a4afe35d05b35a714a383747287bb44281 Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen <3275109+hukkin@users.noreply.github.com> Date: Tue, 24 Sep 2024 21:41:15 +0300 Subject: [PATCH 1/2] Drop support for EOL Python 3.7 and 3.8 --- .github/workflows/tests.yml | 10 +++++----- pyproject.toml | 7 +------ src/typenv/__init__.py | 17 ++--------------- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a39b5ce..4d09051 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,9 +13,9 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12-dev'] + python-version: ['pypy-3.9', '3.9', '3.10', '3.11', '3.12', '3.13-dev'] os: [ubuntu-latest, macos-latest, windows-latest] - continue-on-error: ${{ matrix.python-version == '3.12-dev' }} + continue-on-error: ${{ matrix.python-version == '3.13-dev' }} steps: - uses: actions/checkout@v3 @@ -29,7 +29,7 @@ jobs: run: | pip install . -r tests/requirements.txt pre-commit - name: Linters - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.7' + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' run: | pre-commit run -a - name: Test with pytest @@ -37,7 +37,7 @@ jobs: pytest --cov --cov-fail-under=100 - name: Report coverage - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.7' + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' uses: codecov/codecov-action@v2 pypi-publish: @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 with: - python-version: '3.7' + python-version: '3.12' - name: Install build and publish tools run: | pip install build twine diff --git a/pyproject.toml b/pyproject.toml index 74de972..d3405c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,10 +10,9 @@ authors = [ { name = "Taneli Hukkinen", email = "hukkin@users.noreply.github.com" }, ] license = { file = "LICENSE" } -requires-python = ">=3.7" +requires-python = ">=3.9" dependencies = [ "python-dotenv >=0.10.3", - "typing-extensions >=3.7.4; python_version < '3.8'", ] readme = "README.md" classifiers = [ @@ -21,10 +20,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] diff --git a/src/typenv/__init__.py b/src/typenv/__init__.py index e707b39..2873197 100644 --- a/src/typenv/__init__.py +++ b/src/typenv/__init__.py @@ -8,17 +8,13 @@ import json import os import string -import sys from types import MappingProxyType import typing from typing import Any, NamedTuple, TypeVar, Union import dotenv -if sys.version_info < (3, 8): # pragma: no cover - from typing_extensions import Literal -else: # pragma: no cover - from typing import Literal +from typing import Literal _EMPTY_MAP: MappingProxyType = MappingProxyType({}) @@ -73,8 +69,7 @@ def _cast_bytes(value: str) -> bytes: For now this only deals with hex encoded strings. """ - value = value.lower() - value = _removeprefix(value, "0x") + value = value.lower().removeprefix("0x") if len(value) % 2: value = "0" + value return bytes.fromhex(value) @@ -434,11 +429,3 @@ def _validate_name(self, name: _Str) -> None: raise ValueError( f'Invalid name "{name}": Environment variable name can not start with a number' ) - - -# TODO: replace this with stdlib implementation once min Python version -# is 3.9 -def _removeprefix(string: str, prefix: str) -> str: - if prefix and string.startswith(prefix): - return string[len(prefix) :] - return string From 9c061f8af390742fdc8c29fa5d712253ed373309 Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen <3275109+hukkin@users.noreply.github.com> Date: Tue, 24 Sep 2024 22:12:19 +0300 Subject: [PATCH 2/2] Update and fix pre-commit --- .flake8 | 6 +++-- .pre-commit-config.yaml | 16 ++++++------- README.md | 1 + src/typenv/__init__.py | 53 +++++++++++++---------------------------- tests/test_common.py | 4 ++-- 5 files changed, 32 insertions(+), 48 deletions(-) diff --git a/.flake8 b/.flake8 index 7ebef70..28f8d17 100644 --- a/.flake8 +++ b/.flake8 @@ -2,6 +2,8 @@ max-line-length = 99 max-complexity = 10 extend-ignore = - E203, # E203: Whitespace before ':' (violates PEP8 and black style) - A003, # A003: class attribute is shadowing a python builtin (we violate this on purpose a lot) + # E203: Whitespace before ':' (violates PEP8 and black style) + E203, + # A003: class attribute is shadowing a python builtin (we violate this on purpose a lot) + A003, extend-exclude = */site-packages/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bbeda7a..58d5eb8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,24 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: 8fe62d14e0b4d7d845a7022c5c2c3ae41bdd3f26 # frozen: v4.1.0 + rev: 2c9f875913ee60ca25ce70243dc24d5b6415598c # frozen: v4.6.0 hooks: - id: check-yaml - id: check-toml - repo: https://github.com/pre-commit/pygrep-hooks - rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0 + rev: 3a6eb0fadf60b3cccfd80bad9dbb6fae7e47b316 # frozen: v1.10.0 hooks: - id: python-use-type-annotations - id: python-check-blanket-noqa - repo: https://github.com/PyCQA/isort - rev: c5e8fa75dda5f764d20f66a215d71c21cfa198e1 # frozen: 5.10.1 + rev: c235f5e450b4b84e58d114ed4c589cbf454175a3 # frozen: 5.13.2 hooks: - id: isort - repo: https://github.com/psf/black - rev: ae2c0758c9e61a385df9700dc9c231bf54887041 # frozen: 22.3.0 + rev: b965c2a5026f8ba399283ba3e01898b012853c79 # frozen: 24.8.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: cbeb4c9c4137cff1568659fcc48e8b85cddd0c8d # frozen: 4.0.1 + rev: e43806be3607110919eff72939fda031776e885a # frozen: 7.1.1 hooks: - id: flake8 additional_dependencies: @@ -26,18 +26,18 @@ repos: - flake8-builtins - flake8-comprehensions - repo: https://github.com/myint/docformatter - rev: 67919ee01837761f2d954d7fbb08c12cdd38ec5a # frozen: v1.4 + rev: dfefe062799848234b4cd60b04aa633c0608025e # frozen: v1.7.5 hooks: - id: docformatter - repo: https://github.com/executablebooks/mdformat - rev: b8c05ae822d53326e967da45367d0408afc56a81 # frozen: 0.7.14 + rev: 08fba30538869a440b5059de90af03e3502e35fb # frozen: 0.7.17 hooks: - id: mdformat additional_dependencies: - mdformat-black - mdformat-toc - repo: https://github.com/pre-commit/mirrors-mypy - rev: bdfdfda2221c4fd123dbc9ac0f2074951bd5af58 # frozen: v0.942 + rev: d4911cfb7f1010759fde68da196036feeb25b99d # frozen: v1.11.2 hooks: - id: mypy args: ["--scripts-are-modules"] diff --git a/README.md b/README.md index 221fa17..b4f0b06 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,7 @@ env = Env() # A single validator function NAME = env.str("NAME", validate=lambda n: n.startswith("Harry")) + # A validator function can signal error by raising an exception def is_positive(num): if num <= 0: diff --git a/src/typenv/__init__.py b/src/typenv/__init__.py index 2873197..c6f3991 100644 --- a/src/typenv/__init__.py +++ b/src/typenv/__init__.py @@ -10,13 +10,10 @@ import string from types import MappingProxyType import typing -from typing import Any, NamedTuple, TypeVar, Union +from typing import Any, Literal, NamedTuple, TypeVar, Union import dotenv -from typing import Literal - - _EMPTY_MAP: MappingProxyType = MappingProxyType({}) # Make aliases for these types because typecast method names in `Env` class @@ -140,14 +137,12 @@ def str( *, default: type[_Missing] | _Str = _Missing, validate: Callable | Iterable[Callable] = (), - ) -> _Str: - ... + ) -> _Str: ... @typing.overload def str( self, name: _Str, *, default: None, validate: Callable | Iterable[Callable] = () - ) -> _Str | None: - ... + ) -> _Str | None: ... def str( self, @@ -166,8 +161,7 @@ def bytes( encoding: Literal["hex"], default: type[_Missing] | _Bytes = _Missing, validate: Callable | Iterable[Callable] = (), - ) -> _Bytes: - ... + ) -> _Bytes: ... @typing.overload def bytes( @@ -177,8 +171,7 @@ def bytes( encoding: Literal["hex"], default: None, validate: Callable | Iterable[Callable] = (), - ) -> _Bytes | None: - ... + ) -> _Bytes | None: ... def bytes( self, @@ -197,14 +190,12 @@ def int( *, default: type[_Missing] | _Int = _Missing, validate: Callable | Iterable[Callable] = (), - ) -> _Int: - ... + ) -> _Int: ... @typing.overload def int( self, name: _Str, *, default: None, validate: Callable | Iterable[Callable] = () - ) -> _Int | None: - ... + ) -> _Int | None: ... def int( self, @@ -222,14 +213,12 @@ def bool( *, default: type[_Missing] | _Bool = _Missing, validate: Callable | Iterable[Callable] = (), - ) -> _Bool: - ... + ) -> _Bool: ... @typing.overload def bool( self, name: _Str, *, default: None, validate: Callable | Iterable[Callable] = () - ) -> _Bool | None: - ... + ) -> _Bool | None: ... def bool( self, @@ -247,14 +236,12 @@ def float( *, default: type[_Missing] | _Float = _Missing, validate: Callable | Iterable[Callable] = (), - ) -> _Float: - ... + ) -> _Float: ... @typing.overload def float( self, name: _Str, *, default: None, validate: Callable | Iterable[Callable] = () - ) -> _Float | None: - ... + ) -> _Float | None: ... def float( self, @@ -272,14 +259,12 @@ def decimal( *, default: type[_Missing] | D = _Missing, validate: Callable | Iterable[Callable] = (), - ) -> D: - ... + ) -> D: ... @typing.overload def decimal( self, name: _Str, *, default: None, validate: Callable | Iterable[Callable] = () - ) -> D | None: - ... + ) -> D | None: ... def decimal( self, @@ -309,14 +294,12 @@ def list( *, default: type[_Missing] | _List = _Missing, validate: Callable | Iterable[Callable] = (), - ) -> _List[_Str]: - ... + ) -> _List[_Str]: ... @typing.overload def list( self, name: _Str, *, default: None, validate: Callable | Iterable[Callable] = () - ) -> _List[_Str] | None: - ... + ) -> _List[_Str] | None: ... @typing.overload def list( @@ -326,8 +309,7 @@ def list( default: type[_Missing] | _List[_T] = _Missing, validate: Callable | Iterable[Callable] = (), subcast: Callable[..., _T], - ) -> _List[_T]: - ... + ) -> _List[_T]: ... @typing.overload def list( @@ -337,8 +319,7 @@ def list( default: None, validate: Callable | Iterable[Callable] = (), subcast: Callable[..., _T], - ) -> _List[_T] | None: - ... + ) -> _List[_T] | None: ... def list( self, diff --git a/tests/test_common.py b/tests/test_common.py index 3d6e85b..685b564 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -54,9 +54,9 @@ def is_negative(val): assert env.int("AN_INTEGER", validate=is_positive) == 982 assert env.int("AN_INTEGER", validate=(is_positive, is_positive)) == 982 - with pytest.raises(Exception): + with pytest.raises(Exception, match="AN_INTEGER"): env.int("AN_INTEGER", validate=is_negative) - with pytest.raises(Exception): + with pytest.raises(Exception, match="AN_INTEGER"): env.int("AN_INTEGER", validate=(is_positive, is_negative))