Skip to content

Add an arg_alias decorator. - #83

Open
ilia-kats wants to merge 3 commits into
mainfrom
arg_alias_decorator
Open

Add an arg_alias decorator.#83
ilia-kats wants to merge 3 commits into
mainfrom
arg_alias_decorator

Conversation

@ilia-kats

@ilia-kats ilia-kats commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Many scverse functions accept an axis argument that can be one of 0, "obs", 1, and "var", where "obs" is an alias for 0 and "var" is an alias for 1. The function then usually performs some conversion and checking internally and proceeds with one canonical representation (usually 0 and 1). This decorator generalizes this concept to arbitrary sets of values and aliases.

The rules are encoded in the type hint for the aliased argument. If there is only a single set of aliases, the type hint must be a Literal with the canonical representation as first argument followed by its aliases. If there are multiple sets of aliases, that is multiple semantically different values that the function accepts, as in the axis example above, the type hint must be a Union of Literal s, where each Literal follows the same rules as above: The canonical representation is the first argument followed by its aliases.

Many scverse functions accept an axis argument that can be one
of 0, "obs", 1, and "var", where "obs" is an alias for 0 and
"var" is an alias for 1. The function then usually performs some
conversion and checking internally and proceeds with one canonical
representation (usually 0 and 1). This decorator generalizes this
concept to arbitrary sets of values and aliases. The rules are
encoded in the type hint for the aliased argument. If there is
only a single set of aliases, the type hint must be a Literal with
the canonical representation as first argument followed by its aliases.
If there are multiple sets of aliases, that is multiple semantically
different values that the function accepts, as in the axis example
above, the type hint must be a Union of Literal s, where each Literal
follows the same rules as above: The canonical representation is
the first argument followed by its aliases.
@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.03%. Comparing base (56d757c) to head (3d040b8).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #83      +/-   ##
==========================================
+ Coverage   92.77%   93.03%   +0.26%     
==========================================
  Files          11       12       +1     
  Lines         609      675      +66     
==========================================
+ Hits          565      628      +63     
- Misses         44       47       +3     
Files with missing lines Coverage Δ
src/scverse_misc/__init__.py 100.00% <100.00%> (ø)
src/scverse_misc/_arg_alias.py 100.00% <100.00%> (ø)

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@flying-sheep flying-sheep left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Smart idea!

But interpreting annotations is super hairy, so I think

  • the test matrix should expand to all supported Python versions (3.12, 3.13, 3.14)
  • parametrize tests so functions with string annotations (like e.g. via from __future__ import annotations) and live objects are tested
  • see below

hint = get_type_hints(func)[argname]
if get_origin(hint) is Literal:
sets = (hint,)
elif get_origin(hint) is Union:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this line needs to be tested against both typing.Unions and types.UnionTypes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

In Python 3.14 types.UnionType is an alias for typing.Union. In Python 3.12 and 3.13, Literal | Literal always produces typing.Union.

@flying-sheep flying-sheep Sep 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In Python 3.12 and 3.13, Literal | Literal always produces typing.Union.

$ uvx -p 3.13 python -c "print(type(int | str))"
<class 'types.UnionType'>

I think only with from __future__ import annotations?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No, even without, but only for Literal and other generics: Literal returns a _LiteralGenericAlias, which is a subclass of GenericAlias which defines __or__ and __ror__.

Comment thread src/scverse_misc/_arg_alias.py Outdated
@ilia-kats

Copy link
Copy Markdown
Collaborator Author

The test matrix already covers 3.12, 3.13, and 3.14. I've expanded the tests to cover string annotations, I'm not sure what you mean by live objects.

@flying-sheep

flying-sheep commented Sep 4, 2026

Copy link
Copy Markdown
Member

Sorry I was behind on this, but now I understand it. There are three ways annotations are evaluated:

Let’s take def foo() -> int: ... as example:

  1. Python < 3.14 (default) had foo.__annotations__ == {"return": int}
  2. Python ≥ 3.14 (default) evaluates annotations lazily (see below)
  3. Python with from __future__ import annotations has foo.__annotations__ == {"return": "int"}
  4. A future Python won’t have objects add __annotations__ anymore (and no more from __future__ import annotations)

From Python 3.14 on, object.__annotate__(format) exists and powered by it annotationlib.get_annotations(foo, eval_str=..., format=format), and typing.get_type_hints gained a format parameter too.

So we need to test if what we use (typing.get_type_hints without format specified) works as expected with and without from __future__ import annotations.

@ilia-kats

ilia-kats commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

It was my understanding that from __future__ import annotations is equivalent to manually stringifying the type hints (which the axis_union_string argument does) and that hasn't changed in 3.14.

@flying-sheep

Copy link
Copy Markdown
Member

Yup, but not stringifying them changed in 3.14 (maybe too subtly to make a difference but still).

I need to understand why the Union code path works at all, I think then I’m happy.

@ilia-kats

Copy link
Copy Markdown
Collaborator Author

Sure, but that should be covered by the 3.14 tests with the axis and axis_union arguments. I still don't understand why we need to test the from __future__ import annotations separately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants