Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .github/workflows/formatting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,27 @@ name: "Formatting"
on: [push]
jobs:
black:
# job id and matrix "tool" values are kept as black/isort on purpose: they're the required
# status check names configured in the repo's branch ruleset ("black (black)", "black
# (isort)"). The command each value runs is now ruff, not the tool the label names -
# renaming the labels requires updating the ruleset's required-check list in the same change.
name: "black (${{ matrix.tool }})"
runs-on: ubuntu-latest
strategy:
matrix:
tool: ["black", "isort"]
include:
- tool: black
command: "ruff format --check ."
- tool: isort
command: "ruff check --select I ."
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v9.0.0
with:
python-version: "3.14"
- name: Install dependencies
run: uv sync --group formatting
run: uv sync --group linting
- name: ${{ matrix.tool }} Code Formatter
run: |
uv run --group formatting ${{ matrix.tool }} . --check
uv run --group linting ${{ matrix.command }}
3 changes: 1 addition & 2 deletions .github/workflows/pythonlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ jobs:
- name: Run linting
if: matrix.linter-env == 'linting'
run: |
uv run --group linting pylint src/generics
uv run --group linting pylint unittests --rcfile=unittests/.pylintrc
uv run --group linting ruff check src/generics unittests
- name: Run type_check
if: matrix.linter-env == 'type_check'
run: |
Expand Down
21 changes: 6 additions & 15 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,10 @@ repos:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 26.5.1
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.0
hooks:
- id: black
language_version: python3
- repo: https://github.com/pycqa/isort
rev: 8.0.1
hooks:
- id: isort
name: isort (python)
- id: isort
name: isort (cython)
types: [cython]
- id: isort
name: isort (pyi)
types: [pyi]
- id: ruff-check
args: [--fix]
files: ^(src/generics|unittests)/
- id: ruff-format
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
![Unittests status badge](https://github.com/Hochfrequenz/python-generics/workflows/Unittests/badge.svg)
![Coverage status badge](https://github.com/Hochfrequenz/python-generics/workflows/Coverage/badge.svg)
![Linting status badge](https://github.com/Hochfrequenz/python-generics/workflows/Linting/badge.svg)
![Black status badge](https://github.com/Hochfrequenz/python-generics/workflows/Formatting/badge.svg)
![Formatting status badge](https://github.com/Hochfrequenz/python-generics/workflows/Formatting/badge.svg)

Ever wondered how to do something like this?

Expand Down
36 changes: 22 additions & 14 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,30 @@ dynamic = ["readme", "version"]
Changelog = "https://github.com/Hochfrequenz/python-generics/releases"
Homepage = "https://github.com/Hochfrequenz/python-generics"

[tool.black]
[tool.ruff]
line-length = 120
target_version = ["py310", "py311", "py312", "py313", "py314"]
target-version = "py310"
extend-exclude = ["*.md"]
Comment on lines +29 to +32

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - added target-version = "py310" explicitly to [tool.ruff] in 3e99881, matching requires-python's floor instead of relying on ruff's implicit inference.


[tool.isort]
line_length = 120
profile = "black"
[tool.ruff.per-file-target-version]
"unittests/test_py_312.py" = "py312"

[tool.pylint."MESSAGES CONTROL"]
max-line-length = 120
disable = "fixme,too-few-public-methods"
[tool.ruff.lint]
select = ["E", "W", "F", "I", "UP", "B", "N", "PL", "RUF"]
ignore = [
"PLR0912", # too-many-branches
"PLR0913", # too-many-arguments
"PLR0915", # too-many-statements
"PLR0917", # too-many-positional-arguments
"PLR2004", # magic-value-comparison
]

[tool.ruff.lint.per-file-ignores]
# unittests/.pylintrc disabled invalid-name repo-wide for this directory: tests use single
# uppercase letters (A, B, C, ...) as class/type-alias names to mirror TypeVar conventions.
"unittests/*" = ["N"]

[tool.ruff.format]

[tool.codespell]
skip = "uv.lock"
Expand All @@ -51,7 +64,7 @@ coverage = [
{include-group = "tests"}
]
linting = [
"pylint==4.0.6",
"ruff==0.16.0",
{include-group = "tests"}
]
type_check = [
Expand All @@ -61,17 +74,12 @@ type_check = [
spellcheck = [
"codespell==2.4.3"
]
formatting = [
"black==26.5.1",
"isort==8.0.1"
]
dev = [
{include-group = "tests"},
{include-group = "coverage"},
{include-group = "linting"},
{include-group = "type_check"},
{include-group = "spellcheck"},
{include-group = "formatting"},
"pre-commit"
]

Expand Down
27 changes: 12 additions & 15 deletions src/generics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
is no need to support this edge case.
"""

from typing import Any, Generic
from typing import Any, Generic, Protocol, TypeVar, get_args, get_origin
from typing import GenericAlias as TypesGenericAlias # type: ignore[attr-defined]
from typing import Optional, Protocol, TypeVar, Union
from typing import _GenericAlias as TypingGenericAlias # type: ignore[attr-defined]
from typing import get_args, get_origin


class GenericType(Protocol):
Expand All @@ -22,7 +20,7 @@ class GenericType(Protocol):
__orig_bases__: tuple[type, ...]


def get_type_vars(type_: Union[type, GenericType]) -> tuple[TypeVar, ...]:
def get_type_vars(type_: type | GenericType) -> tuple[TypeVar, ...]:
"""
For a given generic type, return a tuple of its type variables. The type variables are collected through the
supertypes arguments `Generic` if present.
Expand Down Expand Up @@ -73,7 +71,7 @@ def get_type_vars(type_: Union[type, GenericType]) -> tuple[TypeVar, ...]:
return tuple(type_vars)


def _generic_metaclass_executed_on_type(type_: Union[type, GenericType]) -> bool:
def _generic_metaclass_executed_on_type(type_: type | GenericType) -> bool:
"""
This function determines if the type was processed by a `_GenericAlias` with all its `__mro_entries__` magic.
I.e. if the type has `Generic` as supertype or something like `A[T]` in its supertypes.
Expand All @@ -93,25 +91,25 @@ def _generic_metaclass_executed_on_type(type_: Union[type, GenericType]) -> bool
# variables.


def _find_super_type_trace(type_: type, search_for_type: type) -> Optional[list[type]]:
def _find_super_type_trace(type_: type, search_for_type: type) -> list[type] | None:
"""
This function returns a list of ancestors tracing from `type_` to `search_for_type`.
The list is ordered from `type_` to `search_for_type`. If `search_for_type` is not a supertype of
`type_`, `None` is returned.
"""
if type_ == search_for_type:
return [type_]
if type_ == object:
if type_ is object:
return None
for base in type_.__bases__:
super_type_trace = _find_super_type_trace(base, search_for_type)
if super_type_trace is not None:
return [type_] + super_type_trace
return [type_, *super_type_trace]
return None


def _process_inputs_of_get_filled_type(
type_or_instance: Any, type_var_defining_super_type: type, type_var_or_position: Union[TypeVar, int]
type_or_instance: Any, type_var_defining_super_type: type, type_var_or_position: TypeVar | int
) -> tuple[type, type, int]:
"""
This function processes the inputs of `get_filled_type`. It returns a tuple of the filled type, the super type and
Expand Down Expand Up @@ -142,7 +140,7 @@ def _process_inputs_of_get_filled_type(

# pylint: disable=too-many-branches, too-many-locals
def get_filled_type(
type_or_instance: Any, type_var_defining_super_type: type, type_var_or_position: Union[TypeVar, int]
type_or_instance: Any, type_var_defining_super_type: type, type_var_or_position: TypeVar | int
) -> Any:
"""
Determines the type of the `type_var_or_position` defined by the type `type_var_defining_super_type`.
Expand Down Expand Up @@ -190,10 +188,9 @@ def get_filled_type(
for orig_base in type_.__orig_bases__: # type: ignore[attr-defined]
if get_origin(orig_base) == type_trace[-reversed_index + 1]:
orig_base_args = get_args(orig_base)
if len(orig_base_args) < type_var_index:
if len(orig_base_args) <= type_var_index:
raise TypeError(
f"Could not determine the type in {filled_type!r}: "
f"{orig_base!r} has not enough type arguments"
f"Could not determine the type in {filled_type!r}: {orig_base!r} has not enough type arguments"
)
type_var_replacement = orig_base_args[type_var_index]
if not isinstance(type_var_replacement, TypeVar):
Expand All @@ -206,9 +203,9 @@ def get_filled_type(
raise TypeError(f"Could not determine the type in {filled_type!r}: The value of the TypeVar is undefined")

filled_type_args = get_args(filled_type)
if len(filled_type_args) < type_var_index:
if len(filled_type_args) <= type_var_index:
raise TypeError(
f"Could not determine the type in {filled_type!r}: " f"{filled_type!r} has not enough type arguments"
f"Could not determine the type in {filled_type!r}: {filled_type!r} has not enough type arguments"
)

return filled_type_args[type_var_index]
4 changes: 0 additions & 4 deletions unittests/.pylintrc

This file was deleted.

10 changes: 5 additions & 5 deletions unittests/test_get_filled_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
A module with unit tests for the `get_filled_type` function.
"""

from typing import Any, Generic, List, TypeVar
from typing import Any, Generic, TypeVar

import pytest
from pydantic import BaseModel
Expand Down Expand Up @@ -68,7 +68,7 @@ class B(A[T]):
class C(Generic[U]):
pass

class D(B[Z], C[int], List[U], Generic[U, Z]):
class D(B[Z], C[int], list[U], Generic[U, Z]):
pass

assert get_filled_type(D[int, str], A, T) is str
Expand Down Expand Up @@ -121,7 +121,7 @@ class B(A[T]):
class C(Generic[U]):
pass

class D(B[Z], C[int], List[U], Generic[U, Z]):
class D(B[Z], C[int], list[U], Generic[U, Z]):
pass

d = D[int, str]()
Expand All @@ -146,7 +146,7 @@ class B(A[T]):
class C(Generic[U]):
pass

class D(B[Z], C[int], List[U], Generic[U, Z]):
class D(B[Z], C[int], list[U], Generic[U, Z]):
pass

E = D[int, T]
Expand Down Expand Up @@ -258,7 +258,7 @@ def get_type(self) -> Any:
class MySubType(MySuperType[str]):
pass

assert MySubType().get_type() == str
assert MySubType().get_type() is str

def test_builtin_list_as_supertype(self):
"""
Expand Down
Loading
Loading