Skip to content

Commit

Permalink
Use __future__.annotations everywhere (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
radeklat authored Sep 15, 2024
1 parent 5f29289 commit b3f90f2
Show file tree
Hide file tree
Showing 11 changed files with 62 additions and 52 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Types of changes are:

## [Unreleased]

### Fixes

- Use `__future__.annotations` everywhere.

## [4.3.0] - 2024-09-14

### Features
Expand Down
17 changes: 9 additions & 8 deletions src/settings_doc/importing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

import importlib
from functools import lru_cache
from inspect import isclass
from typing import Dict, List, Tuple, Type

import click
from pydantic_settings import BaseSettings
Expand All @@ -11,11 +12,11 @@


@lru_cache
def import_module_path(module_paths: Tuple[str, ...]) -> Dict[Type[BaseSettings], None]:
def import_module_path(module_paths: tuple[str, ...]) -> dict[type[BaseSettings], None]:
if not module_paths:
return {}

settings: Dict[Type[BaseSettings], None] = {}
settings: dict[type[BaseSettings], None] = {}

for module_path in module_paths:
try:
Expand All @@ -26,7 +27,7 @@ def import_module_path(module_paths: Tuple[str, ...]) -> Dict[Type[BaseSettings]
cause = _RELATIVE_IMPORT_ERROR_MSG
raise click.BadParameter(f"Cannot read the module: {cause}") from exc

new_classes: Dict[Type[BaseSettings], None] = {
new_classes: dict[type[BaseSettings], None] = {
obj: None
for obj in vars(module).values()
if isclass(obj) and issubclass(obj, BaseSettings) and obj.__module__.startswith(module_path)
Expand All @@ -49,8 +50,8 @@ def import_module_path(module_paths: Tuple[str, ...]) -> Dict[Type[BaseSettings]


@lru_cache
def import_class_path(class_paths: Tuple[str, ...]) -> Dict[Type[BaseSettings], None]:
settings: Dict[Type[BaseSettings], None] = {}
def import_class_path(class_paths: tuple[str, ...]) -> dict[type[BaseSettings], None]:
settings: dict[type[BaseSettings], None] = {}

for class_path in class_paths:
module, class_name = class_path.rsplit(".", maxsplit=1)
Expand All @@ -75,13 +76,13 @@ def import_class_path(class_paths: Tuple[str, ...]) -> Dict[Type[BaseSettings],
return settings


def module_path_callback(ctx: click.Context, param: click.Parameter, value: List[str]) -> List[str]:
def module_path_callback(ctx: click.Context, param: click.Parameter, value: list[str]) -> list[str]:
del ctx, param
import_module_path(tuple(value))
return value


def class_path_callback(ctx: click.Context, param: click.Parameter, value: List[str]) -> List[str]:
def class_path_callback(ctx: click.Context, param: click.Parameter, value: list[str]) -> list[str]:
del ctx, param
import_class_path(tuple(value))
return value
30 changes: 16 additions & 14 deletions src/settings_doc/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from __future__ import annotations

import itertools
import logging
import re
import shutil
from enum import Enum, auto
from os import listdir
from pathlib import Path
from typing import Dict, Final, Iterator, List, Optional, Tuple, Type
from typing import Final, Iterator

import click
from jinja2 import Environment, FileSystemLoader, Template, select_autoescape
Expand Down Expand Up @@ -40,8 +42,8 @@ def get_template(env: Environment, output_format: OutputFormat) -> Template:


def _model_fields_recursive(
cls: Type[BaseSettings], prefix: str, env_nested_delimiter: Optional[str]
) -> Iterator[Tuple[str, FieldInfo]]:
cls: type[BaseSettings], prefix: str, env_nested_delimiter: str | None
) -> Iterator[tuple[str, FieldInfo]]:
for field_name, model_field in cls.model_fields.items():
if model_field.validation_alias is not None:
if isinstance(model_field.validation_alias, str):
Expand All @@ -63,16 +65,16 @@ def _model_fields_recursive(
yield prefix + field_name, model_field


def _model_fields(cls: Type[BaseSettings]) -> Iterator[Tuple[str, FieldInfo]]:
def _model_fields(cls: type[BaseSettings]) -> Iterator[tuple[str, FieldInfo]]:
yield from _model_fields_recursive(cls, cls.model_config["env_prefix"], cls.model_config["env_nested_delimiter"])


def render(
output_format: OutputFormat,
module_path: Optional[Tuple[str, ...]] = None,
class_path: Optional[Tuple[str, ...]] = None,
module_path: tuple[str, ...] | None = None,
class_path: tuple[str, ...] | None = None,
heading_offset: int = 0,
templates: Optional[Tuple[Path, ...]] = None,
templates: tuple[Path, ...] | None = None,
) -> str:
"""Render the settings documentation."""
if not class_path and not module_path:
Expand All @@ -87,14 +89,14 @@ def render(
if templates is None:
templates = tuple()

settings: Dict[Type[BaseSettings], None] = importing.import_class_path(class_path)
settings: dict[type[BaseSettings], None] = importing.import_class_path(class_path)
settings.update(importing.import_module_path(module_path))

if not settings:
raise ValueError("No sources of data were found.")

fields = itertools.chain.from_iterable(_model_fields(cls) for cls in settings)
classes: Dict[Type[BaseSettings], List[FieldInfo]] = {cls: list(cls.model_fields.values()) for cls in settings}
classes: dict[type[BaseSettings], list[FieldInfo]] = {cls: list(cls.model_fields.values()) for cls in settings}

env = Environment(
loader=FileSystemLoader(templates + (TEMPLATES_FOLDER,)),
Expand Down Expand Up @@ -182,13 +184,13 @@ def render(
"matches found.",
)
def generate(
module_path: Optional[Tuple[str, ...]],
class_path: Optional[Tuple[str, ...]],
module_path: tuple[str, ...] | None,
class_path: tuple[str, ...] | None,
output_format: OutputFormat,
heading_offset: int,
update_file: Optional[Path],
update_between: Tuple[Optional[str], Optional[str]],
templates: Optional[Tuple[Path, ...]],
update_file: Path | None,
update_between: tuple[str | None, str | None],
templates: tuple[Path, ...] | None,
):
"""Formats `pydantic.BaseSettings` into various formats. By default, the output is to STDOUT."""
try:
Expand Down
20 changes: 9 additions & 11 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from collections.abc import Iterable as IterableCollection
from typing import Iterable, List, Literal, Optional, Type, Union
from typing import Iterable, Literal

from click.testing import CliRunner, Result
from jinja2 import Environment, Template
Expand All @@ -12,7 +14,7 @@
def _mock_import_path(
path_type: Literal["class", "module"],
mocker: MockerFixture,
settings: Union[Type[BaseSettings], Iterable[Type[BaseSettings]]],
settings: type[BaseSettings] | Iterable[type[BaseSettings]],
) -> None:
if not isinstance(settings, IterableCollection):
settings = [settings]
Expand All @@ -21,25 +23,21 @@ def _mock_import_path(
mocker.patch(f"settings_doc.importing.import_{path_type}_path", return_value=settings)


def mock_import_class_path(
mocker: MockerFixture, settings: Union[Type[BaseSettings], Iterable[Type[BaseSettings]]]
) -> None:
def mock_import_class_path(mocker: MockerFixture, settings: type[BaseSettings] | Iterable[type[BaseSettings]]) -> None:
_mock_import_path("class", mocker, settings)


def mock_import_module_path(
mocker: MockerFixture, settings: Union[Type[BaseSettings], Iterable[Type[BaseSettings]]]
) -> None:
def mock_import_module_path(mocker: MockerFixture, settings: type[BaseSettings] | Iterable[type[BaseSettings]]) -> None:
_mock_import_path("module", mocker, settings)


def run_app_with_settings(
mocker: MockerFixture,
runner: CliRunner,
settings: Union[Type[BaseSettings], Iterable[Type[BaseSettings]]],
args: Optional[List[str]] = None,
settings: type[BaseSettings] | Iterable[type[BaseSettings]],
args: list[str] | None = None,
fmt: str = "markdown",
template: Union[str, Template, None] = None,
template: str | Template | None = None,
) -> str:
"""Helper function to run common app scenarios.
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/generate/test_import_class_path.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, List, Type
from __future__ import annotations

import pytest
from click import BadParameter
Expand All @@ -23,7 +23,7 @@ class TestImportClassPath:
),
],
)
def should_return(class_paths: List[str], expected_classes: Dict[Type[BaseSettings], None]):
def should_return(class_paths: list[str], expected_classes: dict[type[BaseSettings], None]):
classes = import_class_path(tuple(f"tests.fixtures.valid_settings.{class_path}" for class_path in class_paths))
assert classes == expected_classes

Expand Down
8 changes: 4 additions & 4 deletions tests/integration/generate/test_import_module_path.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Tuple, Type
from __future__ import annotations

import pytest
from _pytest.capture import CaptureFixture
Expand Down Expand Up @@ -75,8 +75,8 @@ class TestImportModulePath:
],
)
def should_return_base_settings_classes(
module_paths: Tuple[str, ...],
expected_classes: Dict[Type[BaseSettings], None],
module_paths: tuple[str, ...],
expected_classes: dict[type[BaseSettings], None],
capsys: CaptureFixture,
):
classes = import_module_path(tuple(f"tests.fixtures.{module_path}" for module_path in module_paths))
Expand Down Expand Up @@ -112,7 +112,7 @@ def should_return_base_settings_classes(
),
],
)
def should_fail_with_bad_parameter_when(class_paths: Tuple[str, ...], error_message: str):
def should_fail_with_bad_parameter_when(class_paths: tuple[str, ...], error_message: str):
with pytest.raises(BadParameter, match=error_message):
import_module_path(class_paths)

Expand Down
7 changes: 4 additions & 3 deletions tests/integration/generate/test_templates.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

import os
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Dict, List

from click.testing import CliRunner
from pytest_mock import MockerFixture
Expand All @@ -13,12 +14,12 @@
class TestTemplatesOverride:
@staticmethod
def should_use_templates_from_given_directories_in_priority_order(runner: CliRunner, mocker: MockerFixture):
stdouts: Dict[str, str] = {}
stdouts: dict[str, str] = {}

with TemporaryDirectory() as folder_low_priority, TemporaryDirectory() as folder_high_priority:
copy_templates(runner, folder_low_priority)
copy_templates(runner, folder_high_priority)
files: List[str] = os.listdir(folder_low_priority)
files: list[str] = os.listdir(folder_low_priority)
args = ["--templates", folder_high_priority, "--templates", folder_low_priority]

folder_high_priority_path = Path(folder_high_priority)
Expand Down
5 changes: 3 additions & 2 deletions tests/integration/generate/test_update_file.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
from tempfile import NamedTemporaryFile
from typing import Tuple

import pytest
from click.testing import CliRunner
Expand All @@ -18,7 +19,7 @@

def _run_app_update(
mocker: MockerFixture, runner: CliRunner, content: str, repetitions: int, *args: str
) -> Tuple[str, str]:
) -> tuple[str, str]:
stdout = ""
try:
with NamedTemporaryFile("w", delete=False) as write_file:
Expand Down
10 changes: 6 additions & 4 deletions tests/integration/test_app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from __future__ import annotations

import os
import sys
from contextlib import contextmanager
from pathlib import Path
from runpy import run_module
from subprocess import PIPE, CompletedProcess, run
from typing import Iterator, List
from typing import Iterator
from unittest.mock import patch

import pytest
Expand Down Expand Up @@ -44,7 +46,7 @@ def patch_pythonpath() -> Iterator[None]:


@pytest.fixture()
def command_line_args() -> List[str]:
def command_line_args() -> list[str]:
return ["generate", "--class", "tests.fixtures.valid_settings.EmptySettings", "--output-format", "markdown"]


Expand All @@ -60,7 +62,7 @@ def should_be_possible_to_install():
pass

@staticmethod
def should_be_executable(command_line_args: List[str]):
def should_be_executable(command_line_args: list[str]):
with install():
with patch_pythonpath():
os.chdir(Path(__file__).parent.parent.parent)
Expand All @@ -71,7 +73,7 @@ def should_be_executable(command_line_args: List[str]):
@pytest.mark.slow
class TestMain:
@staticmethod
def should_be_executable(command_line_args: List[str]):
def should_be_executable(command_line_args: list[str]):
sys.argv = [sys.argv[0]] + command_line_args
with patch_pythonpath():
try:
Expand Down
5 changes: 3 additions & 2 deletions tests/unit/test_changelog.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

import re
from collections import Counter
from pathlib import Path
from typing import List

import pytest

Expand All @@ -13,7 +14,7 @@ def changelog() -> str:


@pytest.fixture(scope="module")
def all_versions(changelog) -> List[str]:
def all_versions(changelog) -> list[str]:
return re.findall(r"## \[([0-9]+\.[0-9]+\.[0-9]+)\]", changelog)


Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_classes_ordering.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Type
from __future__ import annotations

from click.testing import CliRunner
from pydantic_settings import BaseSettings
Expand All @@ -17,7 +17,7 @@
class TestClassesOrdering:
@staticmethod
def should_be_stable(runner: CliRunner, mocker: MockerFixture):
classes: List[Type[BaseSettings]] = [EmptySettings, MultipleSettings, PossibleValuesSettings]
classes: list[type[BaseSettings]] = [EmptySettings, MultipleSettings, PossibleValuesSettings]
expected_cls_names = [_.__name__.lower() for _ in classes]

for i in range(1, 11):
Expand Down

0 comments on commit b3f90f2

Please sign in to comment.