From e0d2b3f98d7fbda796dcec688a4fcef08e501e99 Mon Sep 17 00:00:00 2001 From: Dane Hillard Date: Wed, 4 Sep 2024 20:20:42 -0400 Subject: [PATCH 1/2] Modernize tooling - Replace `pyflakes` with `ruff` - Add `isort` to manage import ordering - Update versions of `pre-commit` hooks and make parity with `tox` envs - Run all hooks against all files - `isort` reordered imports - `pyupgrade` replaced primitives like `List`, `Dict`, and `Set` with their literals, allowed in Python 3.9+, removing need for imports - `pyupgrade` moved `Iterable` from `typing` to `collections.abc` - Fix type checking failures - Remove unused settings in `docs/conf.py` - Add `Protocol` for resolver classes --- .pre-commit-config.yaml | 23 +++++++++++++++++------ CODE_OF_CONDUCT.md | 1 - docs/conf.py | 19 +------------------ pyproject.toml | 3 +++ setup.cfg | 8 +++++--- src/apiron/__init__.py | 6 +++++- src/apiron/client.py | 4 +++- src/apiron/endpoint/__init__.py | 1 - src/apiron/endpoint/endpoint.py | 6 +++--- src/apiron/endpoint/json.py | 9 +++++---- src/apiron/endpoint/streaming.py | 2 +- src/apiron/endpoint/stub.py | 2 +- src/apiron/exceptions.py | 5 +---- src/apiron/service/__init__.py | 1 - src/apiron/service/base.py | 14 +++++++------- src/apiron/service/discoverable.py | 11 ++++++++--- tests/test_client.py | 2 +- 17 files changed, 61 insertions(+), 56 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ed2a235..5072c69 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: # Click through to this repository to see what other goodies are available - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.6.0 hooks: - id: trailing-whitespace # Removes trailing whitespace from lines in all file types - id: end-of-file-fixer # Fixes last line of all file types @@ -22,22 +22,33 @@ repos: - id: no-commit-to-branch # Checks if you're committing to a disallowed branch args: [--branch, dev] - id: check-ast # Checks that Python files are valid syntax + - id: check-toml # Checks TOML files for syntax errors ########## # Python # ########## # pyupgrade updates older syntax to newer syntax. -# It's particularly handy for updating `.format()` calls to f-strings. - repo: https://github.com/asottile/pyupgrade - rev: v2.7.4 + rev: v3.17.0 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py39-plus] # Run black last on Python code so all changes from previous hooks are reformatted - repo: https://github.com/psf/black - rev: 21.5b2 + rev: 24.8.0 hooks: - id: black - language_version: python3.7 + language_version: python3.9 + +- repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.11.1 + hooks: + - id: mypy + additional_dependencies: [typing_extensions, types-requests, types-urllib3] diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 71cd4f0..430b390 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -74,4 +74,3 @@ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.ht For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq - diff --git a/docs/conf.py b/docs/conf.py index 5cd7c0d..b0c0214 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,7 +58,7 @@ "members": True, "show-inheritance": True, } -autodoc_mock_imports = [] + autoclass_content = "both" # Add any paths that contain templates here, relative to this directory. @@ -130,23 +130,6 @@ htmlhelp_basename = "apirondoc" -# -- Options for LaTeX output ------------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). diff --git a/pyproject.toml b/pyproject.toml index dd96eb2..1a727b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,3 +5,6 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 120 target-version = ['py39', 'py310', 'py311', 'py312'] + +[tool.isort] +profile = "black" diff --git a/setup.cfg b/setup.cfg index bd583d8..5d9b010 100644 --- a/setup.cfg +++ b/setup.cfg @@ -103,10 +103,12 @@ commands = skip_install = True deps = black - pyflakes + isort + ruff commands = - pyflakes {posargs:src tests} - black {posargs:--check src tests} + ruff check {posargs:src tests} + black {posargs:--check --diff src tests} + isort {posargs:--check --diff src tests} [testenv:typecheck] deps = diff --git a/src/apiron/__init__.py b/src/apiron/__init__.py index 0029783..2e097f6 100644 --- a/src/apiron/__init__.py +++ b/src/apiron/__init__.py @@ -1,6 +1,10 @@ from apiron.client import Timeout from apiron.endpoint import Endpoint, JsonEndpoint, StreamingEndpoint, StubEndpoint -from apiron.exceptions import APIException, NoHostsAvailableException, UnfulfilledParameterException +from apiron.exceptions import ( + APIException, + NoHostsAvailableException, + UnfulfilledParameterException, +) from apiron.service import DiscoverableService, Service, ServiceBase __all__ = [ diff --git a/src/apiron/client.py b/src/apiron/client.py index d48c1e2..9359d7b 100644 --- a/src/apiron/client.py +++ b/src/apiron/client.py @@ -1,8 +1,9 @@ from __future__ import annotations + import collections import logging import random -from typing import Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any from urllib import parse import requests @@ -11,6 +12,7 @@ if TYPE_CHECKING: import apiron # pragma: no cover + from apiron.exceptions import NoHostsAvailableException LOGGER = logging.getLogger(__name__) diff --git a/src/apiron/endpoint/__init__.py b/src/apiron/endpoint/__init__.py index 96ed7fa..9687248 100644 --- a/src/apiron/endpoint/__init__.py +++ b/src/apiron/endpoint/__init__.py @@ -3,5 +3,4 @@ from apiron.endpoint.streaming import StreamingEndpoint from apiron.endpoint.stub import StubEndpoint - __all__ = ["Endpoint", "JsonEndpoint", "StreamingEndpoint", "StubEndpoint"] diff --git a/src/apiron/endpoint/endpoint.py b/src/apiron/endpoint/endpoint.py index a00cc83..18c8b36 100644 --- a/src/apiron/endpoint/endpoint.py +++ b/src/apiron/endpoint/endpoint.py @@ -4,8 +4,9 @@ import string import sys import warnings +from collections.abc import Iterable from functools import partial, update_wrapper -from typing import Any, Callable, Iterable, TypeVar, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Callable, TypeVar if TYPE_CHECKING: # pragma: no cover if sys.version_info >= (3, 10): @@ -21,10 +22,9 @@ import requests from urllib3.util import retry -from apiron import client, Timeout +from apiron import Timeout, client from apiron.exceptions import UnfulfilledParameterException - LOGGER = logging.getLogger(__name__) diff --git a/src/apiron/endpoint/json.py b/src/apiron/endpoint/json.py index fb9ff43..91f2a52 100644 --- a/src/apiron/endpoint/json.py +++ b/src/apiron/endpoint/json.py @@ -1,5 +1,6 @@ import collections -from typing import Any, Dict, Iterable, Optional +from collections.abc import Iterable +from typing import Any, Optional from apiron.endpoint.endpoint import Endpoint @@ -14,7 +15,7 @@ def __init__( *args, path: str = "/", default_method: str = "GET", - default_params: Optional[Dict[str, Any]] = None, + default_params: Optional[dict[str, Any]] = None, required_params: Optional[Iterable[str]] = None, preserve_order: bool = False, ): @@ -23,7 +24,7 @@ def __init__( ) self.preserve_order = preserve_order - def format_response(self, response) -> Dict[str, Any]: + def format_response(self, response) -> dict[str, Any]: """ Extracts JSON data from the response @@ -40,5 +41,5 @@ def format_response(self, response) -> Dict[str, Any]: return response.json(object_pairs_hook=collections.OrderedDict if self.preserve_order else None) @property - def required_headers(self) -> Dict[str, str]: + def required_headers(self) -> dict[str, str]: return {"Accept": "application/json"} diff --git a/src/apiron/endpoint/streaming.py b/src/apiron/endpoint/streaming.py index 79cd2a7..685e5e1 100644 --- a/src/apiron/endpoint/streaming.py +++ b/src/apiron/endpoint/streaming.py @@ -1,4 +1,4 @@ -from typing import Iterable +from collections.abc import Iterable from apiron.endpoint.endpoint import Endpoint diff --git a/src/apiron/endpoint/stub.py b/src/apiron/endpoint/stub.py index 6d9e774..dbb5da8 100644 --- a/src/apiron/endpoint/stub.py +++ b/src/apiron/endpoint/stub.py @@ -1,4 +1,4 @@ -from typing import Optional, Any +from typing import Any, Optional from apiron.endpoint import Endpoint diff --git a/src/apiron/exceptions.py b/src/apiron/exceptions.py index 8d9d22e..cac30ae 100644 --- a/src/apiron/exceptions.py +++ b/src/apiron/exceptions.py @@ -1,6 +1,3 @@ -from typing import Set - - class APIException(Exception): pass @@ -12,6 +9,6 @@ def __init__(self, service_name: str): class UnfulfilledParameterException(APIException): - def __init__(self, endpoint_path: str, unfulfilled_params: Set[str]): + def __init__(self, endpoint_path: str, unfulfilled_params: set[str]): message = f"The {endpoint_path} endpoint was called without required parameters: {unfulfilled_params}" super().__init__(message) diff --git a/src/apiron/service/__init__.py b/src/apiron/service/__init__.py index cff3e8e..0393014 100644 --- a/src/apiron/service/__init__.py +++ b/src/apiron/service/__init__.py @@ -1,5 +1,4 @@ from apiron.service.base import Service, ServiceBase from apiron.service.discoverable import DiscoverableService - __all__ = ["Service", "ServiceBase", "DiscoverableService"] diff --git a/src/apiron/service/base.py b/src/apiron/service/base.py index 6d7097b..a438b03 100644 --- a/src/apiron/service/base.py +++ b/src/apiron/service/base.py @@ -1,15 +1,15 @@ -from typing import Any, Dict, List, Set +from typing import Any from apiron import Endpoint class ServiceMeta(type): @property - def required_headers(cls) -> Dict[str, str]: + def required_headers(cls) -> dict[str, str]: return cls().required_headers @property - def endpoints(cls) -> Set[Endpoint]: + def endpoints(cls) -> set[Endpoint]: return {attr for attr_name, attr in cls.__dict__.items() if isinstance(attr, Endpoint)} def __str__(cls) -> str: @@ -20,12 +20,12 @@ def __repr__(cls) -> str: class ServiceBase(metaclass=ServiceMeta): - required_headers: Dict[str, Any] = {} + required_headers: dict[str, Any] = {} auth = () - proxies: Dict[str, str] = {} + proxies: dict[str, str] = {} @classmethod - def get_hosts(cls) -> List[str]: + def get_hosts(cls) -> list[str]: """ The fully-qualified hostnames that correspond to this service. These are often determined by asking a load balancer or service discovery mechanism. @@ -48,7 +48,7 @@ class Service(ServiceBase): domain: str @classmethod - def get_hosts(cls) -> List[str]: + def get_hosts(cls) -> list[str]: """ The fully-qualified hostnames that correspond to this service. These are often determined by asking a load balancer or service discovery mechanism. diff --git a/src/apiron/service/discoverable.py b/src/apiron/service/discoverable.py index 3132c79..5ed690a 100644 --- a/src/apiron/service/discoverable.py +++ b/src/apiron/service/discoverable.py @@ -1,8 +1,13 @@ -from typing import List, Type +from typing import Protocol from apiron.service.base import ServiceBase +class Resolver(Protocol): + @staticmethod + def resolve(service_name: str) -> list[str]: ... + + class DiscoverableService(ServiceBase): """ A Service whose hosts are determined via a host resolver. @@ -11,11 +16,11 @@ class DiscoverableService(ServiceBase): and returns a list of host names that correspond to that service. """ - host_resolver_class: Type + host_resolver_class: type[Resolver] service_name: str @classmethod - def get_hosts(cls) -> List[str]: + def get_hosts(cls) -> list[str]: return cls.host_resolver_class.resolve(cls.service_name) def __str__(self) -> str: diff --git a/tests/test_client.py b/tests/test_client.py index 0265c4d..16e78ff 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,7 +3,7 @@ import pytest from urllib3.util import retry -from apiron import client, NoHostsAvailableException, Timeout +from apiron import NoHostsAvailableException, Timeout, client @pytest.fixture From 769bc1b56a31f1f6efee8d0a590ff1df726ccc2c Mon Sep 17 00:00:00 2001 From: Dane Hillard Date: Wed, 4 Sep 2024 20:25:00 -0400 Subject: [PATCH 2/2] Add changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd4034e..b0c59a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ 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). ## [Unreleased] +### Changed +- Modernize package quality tooling and configuration ## [8.0.0] - 2024-09-04 ### Changed