diff --git a/docs/guide/users/checking.md b/docs/guide/users/checking.md
index f8eaad4c..4f29ec81 100644
--- a/docs/guide/users/checking.md
+++ b/docs/guide/users/checking.md
@@ -544,7 +544,7 @@ print(module.special_thing)
> Public object points to a different kind of object
-Changing the kind (attribute, function, class, module) of a public object can *silently* break your users' code.
+Changing the kind (type alias, attribute, function, class, module) of a public object can *silently* break your users' code.
```python title="before"
# your code
diff --git a/docs/guide/users/extending.md b/docs/guide/users/extending.md
index 6228205b..247d6645 100644
--- a/docs/guide/users/extending.md
+++ b/docs/guide/users/extending.md
@@ -102,7 +102,7 @@ If the source code is not available (the modules are built-in or compiled), Grif
Griffe then follows the [Visitor pattern](https://www.wikiwand.com/en/Visitor_pattern) to walk the tree and extract information. For ASTs, Griffe uses its [Visitor agent][griffe.Visitor] and for object trees, it uses its [Inspector agent][griffe.Inspector].
-Sometimes during the walk through the tree (depth-first order), both the visitor and inspector agents will trigger events. These events can be hooked on by extensions to alter or enhance Griffe's behavior. Some hooks will be passed just the current node being visited, others will be passed both the node and an instance of an [Object][griffe.Object] subclass, such as a [Module][griffe.Module], a [Class][griffe.Class], a [Function][griffe.Function], or an [Attribute][griffe.Attribute]. Extensions will therefore be able to modify these instances.
+Sometimes during the walk through the tree (depth-first order), both the visitor and inspector agents will trigger events. These events can be hooked on by extensions to alter or enhance Griffe's behavior. Some hooks will be passed just the current node being visited, others will be passed both the node and an instance of an [Object][griffe.Object] subclass, such as a [Module][griffe.Module], a [Class][griffe.Class], a [Function][griffe.Function], an [Attribute][griffe.Attribute], or a [Type Alias][griffe.TypeAlias]. Extensions will therefore be able to modify these instances.
The following flow chart shows an example of an AST visit. The tree is simplified: actual trees have a lot more nodes like `if/elif/else` nodes, `try/except/else/finally` nodes, [and many more][ast.AST].
@@ -200,8 +200,8 @@ There are two **load events**:
There are 3 generic **analysis events**:
- [`on_node`][griffe.Extension.on_node]: The "on node" events are triggered when the agent (visitor or inspector) starts handling a node in the tree (AST or object tree).
-- [`on_instance`][griffe.Extension.on_instance]: The "on instance" events are triggered when the agent just created an instance of [Module][griffe.Module], [Class][griffe.Class], [Function][griffe.Function], or [Attribute][griffe.Attribute], and added it as a member of its parent. The "on instance" event is **not** triggered when an [Alias][griffe.Alias] is created.
-- [`on_members`][griffe.Extension.on_members]: The "on members" events are triggered when the agent just finished handling all the members of an object. Functions and attributes do not have members, so there are no "on members" events for these two kinds.
+- [`on_instance`][griffe.Extension.on_instance]: The "on instance" events are triggered when the agent just created an instance of [Module][griffe.Module], [Class][griffe.Class], [Function][griffe.Function], [Attribute][griffe.Attribute], or [Type Alias][griffe.TypeAlias], and added it as a member of its parent. The "on instance" event is **not** triggered when an [Alias][griffe.Alias] is created.
+- [`on_members`][griffe.Extension.on_members]: The "on members" events are triggered when the agent just finished handling all the members of an object. Functions, attributes and type aliases do not have members, so there are no "on members" events for these kinds.
There are also specific **analysis events** for each object kind:
@@ -215,6 +215,8 @@ There are also specific **analysis events** for each object kind:
- [`on_function_instance`][griffe.Extension.on_function_instance]
- [`on_attribute_node`][griffe.Extension.on_attribute_node]
- [`on_attribute_instance`][griffe.Extension.on_attribute_instance]
+- [`on_type_alias_node`][griffe.Extension.on_type_alias_node]
+- [`on_type_alias_instance`][griffe.Extension.on_type_alias_instance]
And a special event for aliases:
@@ -315,7 +317,7 @@ class MyExtension(Extension):
The preferred method is to check the type of the received node rather than the agent.
-Since hooks also receive instantiated modules, classes, functions and attributes, most of the time you will not need to use the `node` argument other than for checking its type and deciding what to do based on the result. And since we always add `**kwargs` to the hooks' signatures, you can drop any parameter you don't use from the signature:
+Since hooks also receive instantiated modules, classes, functions, attributes and type aliases, most of the time you will not need to use the `node` argument other than for checking its type and deciding what to do based on the result. And since we always add `**kwargs` to the hooks' signatures, you can drop any parameter you don't use from the signature:
```python
import griffe
@@ -391,7 +393,7 @@ class MyExtension(griffe.Extension):
### Extra data
-All Griffe objects (modules, classes, functions, attributes) can store additional (meta)data in their `extra` attribute. This attribute is a dictionary of dictionaries. The first layer is used as namespacing: each extension writes into its own namespace, or integrates with other projects by reading/writing in their namespaces, according to what they support and document.
+All Griffe objects (modules, classes, functions, attributes, type aliases) can store additional (meta)data in their `extra` attribute. This attribute is a dictionary of dictionaries. The first layer is used as namespacing: each extension writes into its own namespace, or integrates with other projects by reading/writing in their namespaces, according to what they support and document.
```python
import griffe
@@ -563,10 +565,10 @@ See [how to use extensions](#using-extensions) to learn more about how to load a
> - [`Continue`][ast.Continue]
> - [`Del`][ast.Del]
> - [`Delete`][ast.Delete]
+> - [`Dict`][ast.Dict]
>
>
>
-> - [`Dict`][ast.Dict]
> - [`DictComp`][ast.DictComp]
> - [`Div`][ast.Div]
> - `Ellipsis`[^1]
@@ -595,11 +597,11 @@ See [how to use extensions](#using-extensions) to learn more about how to load a
> - [`IsNot`][ast.IsNot]
> - [`JoinedStr`][ast.JoinedStr]
> - [`keyword`][ast.keyword]
+> - [`Lambda`][ast.Lambda]
+> - [`List`][ast.List]
>
> |
>
-> - [`Lambda`][ast.Lambda]
-> - [`List`][ast.List]
> - [`ListComp`][ast.ListComp]
> - [`Load`][ast.Load]
> - [`LShift`][ast.LShift]
@@ -627,11 +629,12 @@ See [how to use extensions](#using-extensions) to learn more about how to load a
> - [`NotEq`][ast.NotEq]
> - [`NotIn`][ast.NotIn]
> - `Num`[^1]
+> - [`Or`][ast.Or]
+> - [`ParamSpec`][ast.ParamSpec]
+> - [`Pass`][ast.Pass]
>
> |
>
-> - [`Or`][ast.Or]
-> - [`Pass`][ast.Pass]
> - `pattern`[^3]
> - [`Pow`][ast.Pow]
> - `Print`[^4]
@@ -650,6 +653,9 @@ See [how to use extensions](#using-extensions) to learn more about how to load a
> - `TryExcept`[^5]
> - `TryFinally`[^6]
> - [`Tuple`][ast.Tuple]
+> - [`TypeAlias`][ast.TypeAlias]
+> - [`TypeVar`][ast.TypeVar]
+> - [`TypeVarTuple`][ast.TypeVarTuple]
> - [`UAdd`][ast.UAdd]
> - [`UnaryOp`][ast.UnaryOp]
> - [`USub`][ast.USub]
diff --git a/docs/guide/users/how-to/support-decorators.md b/docs/guide/users/how-to/support-decorators.md
index d8c203f7..b0fe8da5 100644
--- a/docs/guide/users/how-to/support-decorators.md
+++ b/docs/guide/users/how-to/support-decorators.md
@@ -28,7 +28,7 @@ class MyDecorator(griffe.Extension):
"""An extension to suport my decorator."""
```
-Now we can declare the [`on_instance`][griffe.Extension.on_instance] hook, which receives any kind of Griffe object ([`Module`][griffe.Module], [`Class`][griffe.Class], [`Function`][griffe.Function], [`Attribute`][griffe.Attribute]), or we could use a kind-specific hook such as [`on_module_instance`][griffe.Extension.on_module_instance], [`on_class_instance`][griffe.Extension.on_class_instance], [`on_function_instance`][griffe.Extension.on_function_instance] and [`on_attribute_instance`][griffe.Extension.on_attribute_instance]. For example, if you know your decorator is only ever used on class declarations, it would make sense to use `on_class_instance`.
+Now we can declare the [`on_instance`][griffe.Extension.on_instance] hook, which receives any kind of Griffe object ([`Module`][griffe.Module], [`Class`][griffe.Class], [`Function`][griffe.Function], [`Attribute`][griffe.Attribute], [`TypeAlias`][griffe.TypeAlias]), or we could use a kind-specific hook such as [`on_module_instance`][griffe.Extension.on_module_instance], [`on_class_instance`][griffe.Extension.on_class_instance], [`on_function_instance`][griffe.Extension.on_function_instance], [`on_attribute_instance`][griffe.Extension.on_attribute_instance] and [`on_type_alias_instance`][griffe.Extension.on_type_alias_instance]. For example, if you know your decorator is only ever used on class declarations, it would make sense to use `on_class_instance`.
For the example, lets use the `on_function_instance` hook, which receives `Function` instances.
diff --git a/docs/guide/users/navigating.md b/docs/guide/users/navigating.md
index 4d8a7c8f..49560efd 100644
--- a/docs/guide/users/navigating.md
+++ b/docs/guide/users/navigating.md
@@ -6,6 +6,7 @@ Griffe loads API data into data models. These models provide various attributes
- [`Class`][griffe.Class], representing Python classes;
- [`Function`][griffe.Function], representing Python functions and class methods;
- [`Attribute`][griffe.Attribute], representing object attributes that weren't identified as modules, classes or functions;
+- [`Type Alias`][griffe.TypeAlias], representing Python type aliases;
- [`Alias`][griffe.Alias], representing indirections such as imported objects or class members inherited from parent classes.
When [loading an object](loading.md), Griffe will give you back an instance of one of these models. A few examples:
@@ -84,7 +85,7 @@ To access an object's members, there are a few options:
In particular, Griffe extensions should always use `get_member` instead of the subscript syntax `[]`. The `get_member` method only looks into regular members, while the subscript syntax looks into inherited members too (for classes), which cannot be correctly computed until a package is fully loaded (which is generally not the case when an extension is running).
-- In addition to this, models provide the [`attributes`][griffe.Object.attributes], [`functions`][griffe.Object.functions], [`classes`][griffe.Object.classes] or [`modules`][griffe.Object.modules] attributes, which return only members of the corresponding kind. These attributes are computed dynamically each time (they are Python properties).
+- In addition to this, models provide the [`attributes`][griffe.Object.attributes], [`functions`][griffe.Object.functions], [`classes`][griffe.Object.classes], [`type_aliases`][griffe.Object.type_aliases] or [`modules`][griffe.Object.modules] attributes, which return only members of the corresponding kind. These attributes are computed dynamically each time (they are Python properties).
The same way members are accessed, they can also be set:
@@ -121,7 +122,7 @@ If a base class cannot be resolved during computation of inherited members, Grif
If you want to access all members at once (both declared and inherited), use the [`all_members`][griffe.Object.all_members] attribute. If you want to access only declared members, use the [`members`][griffe.Object] attribute.
-Accessing the [`attributes`][griffe.Object.attributes], [`functions`][griffe.Object.functions], [`classes`][griffe.Object.classes] or [`modules`][griffe.Object.modules] attributes will trigger inheritance computation, so make sure to only access them once everything is loaded by Griffe. Don't try to access inherited members in extensions, while visiting or inspecting modules.
+Accessing the [`attributes`][griffe.Object.attributes], [`functions`][griffe.Object.functions], [`classes`][griffe.Object.classes], [`type_aliases`][griffe.Object.type_aliases] or [`modules`][griffe.Object.modules] attributes will trigger inheritance computation, so make sure to only access them once everything is loaded by Griffe. Don't try to access inherited members in extensions, while visiting or inspecting modules.
#### Limitations
@@ -218,7 +219,7 @@ Aliases chains are never partially resolved: either they are resolved down to th
## Object kind
-The kind of an object (module, class, function, attribute or alias) can be obtained in several ways.
+The kind of an object (module, class, function, attribute, type alias or alias) can be obtained in several ways.
- With the [`kind`][griffe.Object.kind] attribute and the [`Kind`][griffe.Kind] enumeration: `obj.kind is Kind.MODULE`.
@@ -230,7 +231,7 @@ The kind of an object (module, class, function, attribute or alias) can be obtai
When given a set of kinds, the method returns true if the object is of one of the given kinds.
-- With the [`is_module`][griffe.Object.is_module], [`is_class`][griffe.Object.is_class], [`is_function`][griffe.Object.is_function], [`is_attribute`][griffe.Object.is_attribute], and [`is_alias`][griffe.Object.is_alias] attributes.
+- With the [`is_module`][griffe.Object.is_module], [`is_class`][griffe.Object.is_class], [`is_function`][griffe.Object.is_function], [`is_attribute`][griffe.Object.is_attribute], [`is_type_alias`][griffe.Object.is_type_alias], and [`is_alias`][griffe.Object.is_alias] attributes.
Additionally, it is possible to check if an object is a sub-kind of module, with the following attributes:
@@ -351,7 +352,7 @@ After a package is loaded, it is still possible to change the style used for spe
Do note, however, that the `parsed` attribute is cached, and won't be reset when overriding the `parser` or `parser_options` values.
-Docstrings have a [`parent`][griffe.Docstring.parent] field too, that is a reference to their respective module, class, function or attribute.
+Docstrings have a [`parent`][griffe.Docstring.parent] field too, that is a reference to their respective module, class, function, attribute or type alias.
## Model-specific fields
@@ -370,6 +371,7 @@ Models have most fields in common, but also have specific fields.
- [`overloads`][griffe.Class.overloads]: A dictionary to store overloads for class-level methods.
- [`decorators`][griffe.Class.decorators]: The [decorators][griffe.Decorator] applied to the class.
- [`parameters`][griffe.Class.parameters]: The [parameters][griffe.Parameters] of the class' `__init__` method, if any.
+- [`type_parameters`][griffe.Class.type_parameters]: The [type parameters][griffe.TypeParameters] of the class.
### Functions
@@ -377,6 +379,7 @@ Models have most fields in common, but also have specific fields.
- [`overloads`][griffe.Function.overloads]: The overloaded signatures of the function.
- [`parameters`][griffe.Function.parameters]: The [parameters][griffe.Parameters] of the function.
- [`returns`][griffe.Function.returns]: The type annotation of the returned value, in the form of an [expression][griffe.Expr]. The `annotation` field can also be used, for compatibility with attributes.
+- [`type_parameters`][griffe.Function.type_parameters]: The [type parameters][griffe.TypeParameters] of the function.
### Attributes
@@ -385,6 +388,10 @@ Models have most fields in common, but also have specific fields.
- [`deleter`][griffe.Attribute.deleter]: The property deleter.
- [`setter`][griffe.Attribute.setter]: The property setter.
+### Type aliases
+- [`value`][griffe.TypeAlias.value]: The value of the type alias, in the form of an [expression][griffe.Expr].
+- [`type_parameters`][griffe.TypeAlias.type_parameters]: The [type parameters][griffe.TypeParameters] of the type alias.
+
### Alias
- [`alias_lineno`][griffe.Alias.alias_lineno]: The alias line number (where the object is imported).
diff --git a/docs/guide/users/recommendations/docstrings.md b/docs/guide/users/recommendations/docstrings.md
index 4cc7a0c3..22539307 100644
--- a/docs/guide/users/recommendations/docstrings.md
+++ b/docs/guide/users/recommendations/docstrings.md
@@ -4,11 +4,14 @@ Here are explanations on what docstrings are, and a few recommendations on how t
## Definition
-A docstring is a line or block of text describing objects such as modules, classes, functions and attributes. They are written below the object signature or assignment, or appear as first expression in a module:
+A docstring is a line or block of text describing objects such as modules, classes, functions, attributes and type aliases. They are written below the object signature or assignment, or appear as first expression in a module:
```python title="module.py"
"""This is the module docstring."""
+type X = dict[str, int]
+"""This is a type alias docstring."""
+
a = 0
"""This is an attribute docstring."""
@@ -51,7 +54,7 @@ Whatever markup you choose, try to stay consistent within your code base.
## Styles
-Docstrings can be written for modules, classes, functions, and attributes. But there are other aspects of a Python API that need to be documented, such as function parameters, returned values, and raised exceptions, to name a few. We could document everything in natural language, but that would make it hard for downstream tools such as documentation generators to extract information in a structured way, to allow dedicated rendering such as tables for parameters.
+Docstrings can be written for modules, classes, functions, attributes, and type aliases. But there are other aspects of a Python API that need to be documented, such as function parameters, returned values, and raised exceptions, to name a few. We could document everything in natural language, but that would make it hard for downstream tools such as documentation generators to extract information in a structured way, to allow dedicated rendering such as tables for parameters.
To compensate for the lack of structure in natural languages, docstring "styles" emerged. A docstring style is a micro-format for docstrings, allowing to structure the information by following a specific format. With the most popular Google and Numpydoc styles, information in docstrings is decomposed into **sections** of different kinds, for example "parameter" sections or "return" sections. Some kinds of section then support documenting multiple items, or support a single block of markup. For example, we can document multiple parameters in "parameter" sections, but a "note" section is only composed of a text block.
@@ -136,7 +139,7 @@ When documenting objects acting as namespaces (modules, classes, enumerations),
## Modules
-Module docstrings should briefly explain what the module contains, and for what purposes these objects can be used. If the documentation generator you chose does not support generating member summaries automatically, you might want to add docstrings sections for attributes, functions, classes and submodules.
+Module docstrings should briefly explain what the module contains, and for what purposes these objects can be used. If the documentation generator you chose does not support generating member summaries automatically, you might want to add docstrings sections for attributes, functions, classes, type aliases and submodules.
```python title="package/__init__.py"
"""A generic package to demonstrate docstrings.
@@ -305,6 +308,19 @@ class GhostTown:
"""The town's size."""
```
+## Type aliases
+
+Type alias docstrings are written below their assignment. As usual, they should have a short summary, and an optional, longer body.
+
+```python
+type Callback = typing.Callable[[int, str], typing.Any]
+"""Callback type for Frobnicators.
+
+The first argument is the number of rounds to run, the second argument
+is the name of the widget being frobnicated.
+"""
+```
+
## Exceptions, warnings
Callables that raise exceptions or emit warnings can document each of these exceptions and warnings. Documenting them informs your users that they could or should catch the raised exceptions, or that they could filter or configure warnings differently. The description next to each exception or warning should explain how or when they are raised or emitted.
diff --git a/docs/guide/users/serializing.md b/docs/guide/users/serializing.md
index 33a33d15..6adcfb4e 100644
--- a/docs/guide/users/serializing.md
+++ b/docs/guide/users/serializing.md
@@ -52,7 +52,7 @@ See all the options for the `dump` command in the [CLI reference](../../referenc
## Python API
-If you have read through the [Navigating](navigating.md) chapter, you know about our five data models for modules, classes, functions, attributes and aliases. Each one of these model provide the two following methods:
+If you have read through the [Navigating](navigating.md) chapter, you know about our six data models for modules, classes, functions, attributes, type aliases and aliases. Each one of these model provide the two following methods:
- [`as_json`][griffe.Object.as_json], which allows to serialize an object into JSON,
- [`from_json`][griffe.Object.from_json], which allows loading JSON back into a model instance.
diff --git a/docs/introduction.md b/docs/introduction.md
index a145628a..a7226fa6 100644
--- a/docs/introduction.md
+++ b/docs/introduction.md
@@ -1,6 +1,6 @@
# Introduction
-Griffe is able to read Python source code and inspect objects at runtime to extract information about the API of a Python package. This information is then stored into data models (Python classes), and these model instances together form a tree that statically represent the package's API: starting with the top-level module, then descending into submodules, classes, functions and attributes. From there, it's possible to explore and exploit this API representation in various ways.
+Griffe is able to read Python source code and inspect objects at runtime to extract information about the API of a Python package. This information is then stored into data models (Python classes), and these model instances together form a tree that statically represent the package's API: starting with the top-level module, then descending into submodules, classes, functions, attributes and type aliases. From there, it's possible to explore and exploit this API representation in various ways.
## Command line tool
diff --git a/docs/reference/api/docstrings/models.md b/docs/reference/api/docstrings/models.md
index 5cb05e09..17f3bdfd 100644
--- a/docs/reference/api/docstrings/models.md
+++ b/docs/reference/api/docstrings/models.md
@@ -14,6 +14,8 @@
::: griffe.DocstringSectionOtherParameters
+::: griffe.DocstringSectionTypeParameters
+
::: griffe.DocstringSectionRaises
::: griffe.DocstringSectionWarns
@@ -32,6 +34,8 @@
::: griffe.DocstringSectionClasses
+::: griffe.DocstringSectionTypeAliases
+
::: griffe.DocstringSectionModules
::: griffe.DocstringSectionDeprecated
@@ -56,12 +60,16 @@
::: griffe.DocstringParameter
+::: griffe.DocstringTypeParameter
+
::: griffe.DocstringAttribute
::: griffe.DocstringFunction
::: griffe.DocstringClass
+::: griffe.DocstringTypeAlias
+
::: griffe.DocstringModule
## **Models base classes**
diff --git a/docs/reference/api/models.md b/docs/reference/api/models.md
index 37f2bcfa..eb775b18 100644
--- a/docs/reference/api/models.md
+++ b/docs/reference/api/models.md
@@ -2,19 +2,20 @@
Griffe stores information extracted from Python source code into data models.
-These models represent trees of objects, starting with modules, and containing classes, functions, and attributes.
+These models represent trees of objects, starting with modules, and containing classes, functions, attributes, and type aliases.
-Modules can have submodules, classes, functions and attributes. Classes can have nested classes, methods and attributes. Functions and attributes do not have any members.
+Modules can have submodules, classes, functions, attributes, and type aliases. Classes can have nested classes, methods, attributes, and type aliases. Functions and attributes do not have any members.
Indirections to objects declared in other modules are represented as "aliases". An alias therefore represents an imported object, and behaves almost exactly like the object it points to: it is a light wrapper around the object, with special methods and properties that allow to access the target's data transparently.
-The 5 models:
+The 6 models:
- [`Module`][griffe.Module]
- [`Class`][griffe.Class]
- [`Function`][griffe.Function]
- [`Attribute`][griffe.Attribute]
- [`Alias`][griffe.Alias]
+- [`TypeAlias`][griffe.TypeAlias]
## **Model kind enumeration**
@@ -33,3 +34,11 @@ The 5 models:
::: griffe.ObjectAliasMixin
::: griffe.Object
+
+## **Models type parameter**
+
+::: griffe.TypeParameters
+
+::: griffe.TypeParameter
+
+::: griffe.TypeParameterKind
diff --git a/docs/reference/api/models/type_alias.md b/docs/reference/api/models/type_alias.md
new file mode 100644
index 00000000..367d906d
--- /dev/null
+++ b/docs/reference/api/models/type_alias.md
@@ -0,0 +1,3 @@
+# Type Alias
+
+::: griffe.TypeAlias
diff --git a/docs/schema.json b/docs/schema.json
index cdbcfd60..08c51884 100644
--- a/docs/schema.json
+++ b/docs/schema.json
@@ -63,7 +63,8 @@
"module",
"class",
"function",
- "attribute"
+ "attribute",
+ "type alias"
]
},
"path": {
@@ -183,7 +184,8 @@
"parameters": true,
"returns": true,
"value": true,
- "annotation": true
+ "annotation": true,
+ "type_parameters": true
},
"additionalProperties": false,
"required": [
@@ -248,6 +250,14 @@
"endlineno"
]
}
+ },
+ "type_parameters": {
+ "title": "For classes, their type parameters.",
+ "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/class/#griffe.Class.type_parameters",
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/type_parameter"
+ }
}
},
"required": [
@@ -304,6 +314,14 @@
"title": "For functions, their return annotation.",
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/function/#griffe.Function.returns",
"$ref": "#/$defs/annotation"
+ },
+ "type_parameters": {
+ "title": "For functions, their type parameters.",
+ "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/function/#griffe.Function.type_parameters",
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/type_parameter"
+ }
}
},
"required": [
@@ -334,6 +352,35 @@
}
}
}
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "type alias"
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "value": {
+ "title": "For type aliases, their value.",
+ "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/type_alias/#griffe.TypeAlias.value",
+ "$ref": "#/$defs/annotation"
+ },
+ "type_parameters": {
+ "title": "For type aliases, their type parameters.",
+ "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/type_alias/#griffe.TypeAlias.type_parameters",
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/type_parameter"
+ }
+ }
+ },
+ "required": [
+ "value"
+ ]
+ }
}
]
}
@@ -355,6 +402,44 @@
"$ref": "#/$defs/expression"
}
]
+ },
+ "type_parameter": {
+ "title": "Type Parameter.",
+ "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.TypeParameter",
+ "type": "object",
+ "properties": {
+ "name": {
+ "title": "The type parameter name.",
+ "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.TypeParameter.name",
+ "type": "string"
+ },
+ "kind": {
+ "title": "The type parameter kind.",
+ "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.TypeParameter.kind",
+ "enum": [
+ "type-var",
+ "type-var-tuple",
+ "param-spec"
+ ]
+ },
+ "annotation": {
+ "title": "The type parameter bound or constraints.",
+ "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.TypeParameter.annotation",
+ "$ref": "#/$defs/annotation"
+ },
+ "default": {
+ "title": "The type parameter default.",
+ "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.TypeParameter.default",
+ "$ref": "#/$defs/annotation"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "kind",
+ "annotation",
+ "default"
+ ]
}
}
-}
\ No newline at end of file
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index cb3d45ea..88d1ff44 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -95,6 +95,7 @@ nav:
- Class: reference/api/models/class.md
- Function: reference/api/models/function.md
- Attribute: reference/api/models/attribute.md
+ - Type Alias: reference/api/models/type_alias.md
- Alias: reference/api/models/alias.md
- Agents: reference/api/agents.md
- Serializers: reference/api/serializers.md
diff --git a/src/_griffe/agents/inspector.py b/src/_griffe/agents/inspector.py
index bb6e5b3a..8efd75b4 100644
--- a/src/_griffe/agents/inspector.py
+++ b/src/_griffe/agents/inspector.py
@@ -4,19 +4,35 @@
from __future__ import annotations
import ast
+import functools
+import sys
+import types
+import typing
from inspect import Parameter as SignatureParameter
-from inspect import Signature, cleandoc, getsourcelines
+from inspect import Signature, cleandoc, getsourcelines, unwrap
from inspect import signature as getsignature
from typing import TYPE_CHECKING, Any
from _griffe.agents.nodes.runtime import ObjectNode
from _griffe.collections import LinesCollection, ModulesCollection
-from _griffe.enumerations import Kind, ParameterKind
-from _griffe.expressions import safe_get_annotation
+from _griffe.enumerations import Kind, ParameterKind, TypeParameterKind
+from _griffe.expressions import Expr, ExprBinOp, ExprSubscript, ExprTuple, safe_get_annotation
from _griffe.extensions.base import Extensions, load_extensions
from _griffe.importer import dynamic_import
from _griffe.logger import logger
-from _griffe.models import Alias, Attribute, Class, Docstring, Function, Module, Parameter, Parameters
+from _griffe.models import (
+ Alias,
+ Attribute,
+ Class,
+ Docstring,
+ Function,
+ Module,
+ Parameter,
+ Parameters,
+ TypeAlias,
+ TypeParameter,
+ TypeParameters,
+)
if TYPE_CHECKING:
from collections.abc import Sequence
@@ -24,8 +40,14 @@
from _griffe.docstrings.parsers import DocstringStyle
from _griffe.enumerations import Parser
- from _griffe.expressions import Expr
+_TYPING_MODULES: tuple[types.ModuleType, ...]
+try:
+ import typing_extensions
+except ImportError:
+ _TYPING_MODULES = (typing,)
+else:
+ _TYPING_MODULES = (typing, typing_extensions)
_empty = Signature.empty
@@ -317,6 +339,7 @@ def inspect_class(self, node: ObjectNode) -> None:
name=node.name,
docstring=self._get_docstring(node),
bases=bases,
+ type_parameters=TypeParameters(*_convert_type_parameters(node.obj, parent=self.current, member=node.name)),
lineno=lineno,
endlineno=endlineno,
)
@@ -436,13 +459,16 @@ def handle_function(self, node: ObjectNode, labels: set | None = None) -> None:
returns = None
else:
parameters = Parameters(
- *[_convert_parameter(parameter, parent=self.current) for parameter in signature.parameters.values()],
+ *[
+ _convert_parameter(parameter, parent=self.current, member=node.name)
+ for parameter in signature.parameters.values()
+ ],
)
return_annotation = signature.return_annotation
returns = (
None
if return_annotation is _empty
- else _convert_object_to_annotation(return_annotation, parent=self.current)
+ else _convert_object_to_annotation(return_annotation, parent=self.current, member=node.name)
)
lineno, endlineno = self._get_linenos(node)
@@ -463,6 +489,9 @@ def handle_function(self, node: ObjectNode, labels: set | None = None) -> None:
name=node.name,
parameters=parameters,
returns=returns,
+ type_parameters=TypeParameters(
+ *_convert_type_parameters(node.obj, parent=self.current, member=node.name),
+ ),
docstring=self._get_docstring(node),
lineno=lineno,
endlineno=endlineno,
@@ -475,6 +504,30 @@ def handle_function(self, node: ObjectNode, labels: set | None = None) -> None:
else:
self.extensions.call("on_function_instance", node=node, func=obj, agent=self)
+ def inspect_type_alias(self, node: ObjectNode) -> None:
+ """Inspect a type alias.
+
+ Parameters:
+ node: The node to inspect.
+ """
+ self.extensions.call("on_node", node=node, agent=self)
+ self.extensions.call("on_type_alias_node", node=node, agent=self)
+
+ lineno, endlineno = self._get_linenos(node)
+
+ type_alias = TypeAlias(
+ name=node.name,
+ value=_convert_type_to_annotation(node.obj.__value__, parent=self.current, member=node.name),
+ lineno=lineno,
+ endlineno=endlineno,
+ type_parameters=TypeParameters(*_convert_type_parameters(node.obj, parent=self.current, member=node.name)),
+ docstring=self._get_docstring(node),
+ parent=self.current,
+ )
+ self.current.set_member(node.name, type_alias)
+ self.extensions.call("on_instance", node=node, obj=type_alias, agent=self)
+ self.extensions.call("on_type_alias_instance", node=node, type_alias=type_alias, agent=self)
+
def inspect_attribute(self, node: ObjectNode) -> None:
"""Inspect an attribute.
@@ -531,7 +584,7 @@ def handle_attribute(self, node: ObjectNode, annotation: str | Expr | None = Non
self.extensions.call("on_attribute_instance", node=node, attr=attribute, agent=self)
-_kind_map = {
+_parameter_kind_map = {
SignatureParameter.POSITIONAL_ONLY: ParameterKind.positional_only,
SignatureParameter.POSITIONAL_OR_KEYWORD: ParameterKind.positional_or_keyword,
SignatureParameter.VAR_POSITIONAL: ParameterKind.var_positional,
@@ -540,12 +593,19 @@ def handle_attribute(self, node: ObjectNode, annotation: str | Expr | None = Non
}
-def _convert_parameter(parameter: SignatureParameter, parent: Module | Class) -> Parameter:
+def _convert_parameter(
+ parameter: SignatureParameter,
+ *,
+ parent: Module | Class,
+ member: str | None = None,
+) -> Parameter:
name = parameter.name
annotation = (
- None if parameter.annotation is _empty else _convert_object_to_annotation(parameter.annotation, parent=parent)
+ None
+ if parameter.annotation is _empty
+ else _convert_object_to_annotation(parameter.annotation, parent=parent, member=member)
)
- kind = _kind_map[parameter.kind]
+ kind = _parameter_kind_map[parameter.kind]
if parameter.default is _empty:
default = None
elif hasattr(parameter.default, "__name__"):
@@ -556,7 +616,7 @@ def _convert_parameter(parameter: SignatureParameter, parent: Module | Class) ->
return Parameter(name, annotation=annotation, kind=kind, default=default)
-def _convert_object_to_annotation(obj: Any, parent: Module | Class) -> str | Expr | None:
+def _convert_object_to_annotation(obj: Any, *, parent: Module | Class, member: str | None = None) -> str | Expr | None:
# Even when *we* import future annotations,
# the object from which we get a signature
# can come from modules which did *not* import them,
@@ -577,4 +637,81 @@ def _convert_object_to_annotation(obj: Any, parent: Module | Class) -> str | Exp
annotation_node = compile(annotation, mode="eval", filename="<>", flags=ast.PyCF_ONLY_AST, optimize=2)
except SyntaxError:
return obj
- return safe_get_annotation(annotation_node.body, parent=parent) # type: ignore[attr-defined]
+ return safe_get_annotation(annotation_node.body, parent, member=member) # type: ignore[attr-defined]
+
+
+_type_parameter_kind_map = {
+ getattr(module, attr): value
+ for attr, value in {
+ "TypeVar": TypeParameterKind.type_var,
+ "TypeVarTuple": TypeParameterKind.type_var_tuple,
+ "ParamSpec": TypeParameterKind.param_spec,
+ }.items()
+ for module in _TYPING_MODULES
+ if hasattr(module, attr)
+}
+
+
+def _convert_type_parameters(
+ obj: Any,
+ *,
+ parent: Module | Class,
+ member: str | None = None,
+) -> list[TypeParameter]:
+ obj = unwrap(obj)
+
+ if not hasattr(obj, "__type_params__"):
+ return []
+
+ type_parameters = []
+ for type_parameter in obj.__type_params__:
+ bound = getattr(type_parameter, "__bound__", None)
+ if bound is not None:
+ bound = _convert_type_to_annotation(bound, parent=parent, member=member)
+ constraints: list[str | Expr] = [
+ _convert_type_to_annotation(constraint, parent=parent, member=member) # type: ignore[misc]
+ for constraint in getattr(type_parameter, "__constraints__", ())
+ ]
+
+ if getattr(type_parameter, "has_default", lambda: False)():
+ default = _convert_type_to_annotation(
+ type_parameter.__default__,
+ parent=parent,
+ member=member,
+ )
+ else:
+ default = None
+
+ type_parameters.append(
+ TypeParameter(
+ type_parameter.__name__,
+ kind=_type_parameter_kind_map[type(type_parameter)],
+ bound=bound,
+ constraints=constraints or None,
+ default=default,
+ ),
+ )
+
+ return type_parameters
+
+
+def _convert_type_to_annotation(obj: Any, *, parent: Module | Class, member: str | None = None) -> str | Expr | None:
+ origin = typing.get_origin(obj)
+
+ if origin is None:
+ return _convert_object_to_annotation(obj, parent=parent, member=member)
+
+ args: Sequence[str | Expr | None] = [
+ _convert_type_to_annotation(arg, parent=parent, member=member) for arg in typing.get_args(obj)
+ ]
+
+ # YORE: EOL 3.9: Replace block with lines 2-3.
+ if sys.version_info >= (3, 10):
+ if origin is types.UnionType:
+ return functools.reduce(lambda left, right: ExprBinOp(left, "|", right), args) # type: ignore[arg-type]
+
+ origin = _convert_type_to_annotation(origin, parent=parent, member=member)
+ if origin is None:
+ return None
+
+ return ExprSubscript(origin, ExprTuple(args, implicit=True)) # type: ignore[arg-type]
diff --git a/src/_griffe/agents/nodes/runtime.py b/src/_griffe/agents/nodes/runtime.py
index 7c1c2c0c..9ec75dfe 100644
--- a/src/_griffe/agents/nodes/runtime.py
+++ b/src/_griffe/agents/nodes/runtime.py
@@ -4,6 +4,7 @@
import inspect
import sys
+import typing
from functools import cached_property
from types import GetSetDescriptorType
from typing import TYPE_CHECKING, Any, ClassVar
@@ -12,8 +13,18 @@
from _griffe.logger import logger
if TYPE_CHECKING:
+ import types
from collections.abc import Sequence
+_TYPING_MODULES: tuple[types.ModuleType, ...]
+try:
+ import typing_extensions
+except ImportError:
+ _TYPING_MODULES = (typing,)
+else:
+ _TYPING_MODULES = (typing, typing_extensions)
+
+
_builtin_module_names = {_.lstrip("_") for _ in sys.builtin_module_names}
_cyclic_relationships = {
("os", "nt"),
@@ -135,6 +146,8 @@ def kind(self) -> ObjectKind:
return ObjectKind.GETSET_DESCRIPTOR
if self.is_property:
return ObjectKind.PROPERTY
+ if self.is_type_alias:
+ return ObjectKind.TYPE_ALIAS
return ObjectKind.ATTRIBUTE
@cached_property
@@ -162,6 +175,14 @@ def is_function(self) -> bool:
# `inspect.isfunction` returns `False` for partials.
return inspect.isfunction(self.obj) or (callable(self.obj) and not self.is_class)
+ @cached_property
+ def is_type_alias(self) -> bool:
+ """Whether this node's object is a type alias."""
+ return isinstance(
+ self.obj,
+ tuple(module.TypeAliasType for module in _TYPING_MODULES if hasattr(module, "TypeAliasType")),
+ )
+
@cached_property
def is_builtin_function(self) -> bool:
"""Whether this node's object is a builtin function."""
diff --git a/src/_griffe/agents/visitor.py b/src/_griffe/agents/visitor.py
index 77f12bf6..b3dcf204 100644
--- a/src/_griffe/agents/visitor.py
+++ b/src/_griffe/agents/visitor.py
@@ -4,8 +4,9 @@
from __future__ import annotations
import ast
+import sys
from contextlib import suppress
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Final
from _griffe.agents.nodes.assignments import get_instance_names, get_names
from _griffe.agents.nodes.ast import (
@@ -18,7 +19,7 @@
from _griffe.agents.nodes.imports import relative_to_absolute
from _griffe.agents.nodes.parameters import get_parameters
from _griffe.collections import LinesCollection, ModulesCollection
-from _griffe.enumerations import Kind
+from _griffe.enumerations import Kind, TypeParameterKind
from _griffe.exceptions import AliasResolutionError, CyclicAliasError, LastNodeError
from _griffe.expressions import (
Expr,
@@ -29,7 +30,20 @@
safe_get_expression,
)
from _griffe.extensions.base import Extensions, load_extensions
-from _griffe.models import Alias, Attribute, Class, Decorator, Docstring, Function, Module, Parameter, Parameters
+from _griffe.models import (
+ Alias,
+ Attribute,
+ Class,
+ Decorator,
+ Docstring,
+ Function,
+ Module,
+ Parameter,
+ Parameters,
+ TypeAlias,
+ TypeParameter,
+ TypeParameters,
+)
if TYPE_CHECKING:
from pathlib import Path
@@ -191,6 +205,43 @@ def _get_docstring(self, node: ast.AST, *, strict: bool = False) -> Docstring |
parser_options=self.docstring_options,
)
+ # YORE: EOL 3.11: Replace block with lines 2-36.
+ if sys.version_info >= (3, 12):
+ _type_parameter_kind_map: Final[dict[type[ast.type_param], TypeParameterKind]] = {
+ ast.TypeVar: TypeParameterKind.type_var,
+ ast.TypeVarTuple: TypeParameterKind.type_var_tuple,
+ ast.ParamSpec: TypeParameterKind.param_spec,
+ }
+
+ def _get_type_parameters(
+ self,
+ node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.TypeAlias,
+ *,
+ scope: str | None = None,
+ ) -> list[TypeParameter]:
+ return [
+ TypeParameter(
+ type_param.name, # type: ignore[attr-defined]
+ kind=self._type_parameter_kind_map[type(type_param)],
+ bound=safe_get_annotation(getattr(type_param, "bound", None), parent=self.current, member=scope),
+ default=safe_get_annotation(
+ getattr(type_param, "default_value", None),
+ parent=self.current,
+ member=scope,
+ ),
+ )
+ for type_param in node.type_params
+ ]
+ else:
+
+ def _get_type_parameters(
+ self,
+ node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef, # noqa: ARG002,
+ *,
+ scope: str | None = None, # noqa: ARG002,
+ ) -> list[TypeParameter]:
+ return []
+
def get_module(self) -> Module:
"""Build and return the object representing the module attached to this visitor.
@@ -269,7 +320,7 @@ def visit_classdef(self, node: ast.ClassDef) -> None:
lineno = node.lineno
# Handle base classes.
- bases = [safe_get_base_class(base, parent=self.current) for base in node.bases]
+ bases = [safe_get_base_class(base, parent=self.current, member=node.name) for base in node.bases]
class_ = Class(
name=node.name,
@@ -277,10 +328,12 @@ def visit_classdef(self, node: ast.ClassDef) -> None:
endlineno=node.end_lineno,
docstring=self._get_docstring(node),
decorators=decorators,
+ type_parameters=TypeParameters(*self._get_type_parameters(node, scope=node.name)),
bases=bases, # type: ignore[arg-type]
runtime=not self.type_guarded,
)
class_.labels |= self.decorators_to_labels(decorators)
+
self.current.set_member(node.name, class_)
self.current = class_
self.extensions.call("on_instance", node=node, obj=class_, agent=self)
@@ -369,7 +422,7 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
attribute = Attribute(
name=node.name,
value=None,
- annotation=safe_get_annotation(node.returns, parent=self.current),
+ annotation=safe_get_annotation(node.returns, parent=self.current, member=node.name),
lineno=node.lineno,
endlineno=node.end_lineno,
docstring=self._get_docstring(node),
@@ -387,7 +440,7 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
Parameter(
name,
kind=kind,
- annotation=safe_get_annotation(annotation, parent=self.current),
+ annotation=safe_get_annotation(annotation, parent=self.current, member=node.name),
default=default
if isinstance(default, str)
else safe_get_expression(default, parent=self.current, parse_strings=False),
@@ -401,8 +454,9 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
lineno=lineno,
endlineno=node.end_lineno,
parameters=parameters,
- returns=safe_get_annotation(node.returns, parent=self.current),
+ returns=safe_get_annotation(node.returns, parent=self.current, member=node.name),
decorators=decorators,
+ type_parameters=TypeParameters(*self._get_type_parameters(node, scope=node.name)),
docstring=self._get_docstring(node),
runtime=not self.type_guarded,
parent=self.current,
@@ -451,6 +505,44 @@ def visit_asyncfunctiondef(self, node: ast.AsyncFunctionDef) -> None:
"""
self.handle_function(node, labels={"async"})
+ # YORE: EOL 3.11: Replace block with lines 2-36.
+ if sys.version_info >= (3, 12):
+
+ def visit_typealias(self, node: ast.TypeAlias) -> None:
+ """Visit a type alias node.
+
+ Parameters:
+ node: The node to visit.
+ """
+ self.extensions.call("on_node", node=node, agent=self)
+ self.extensions.call("on_type_alias_node", node=node, agent=self)
+
+ # A type alias's name attribute is syntactically a single NAME,
+ # but represented as an expression in the AST.
+ # https://jellezijlstra.github.io/pep695#ast
+
+ name = node.name.id
+
+ value = safe_get_expression(node.value, parent=self.current, member=name)
+
+ try:
+ docstring = self._get_docstring(ast_next(node), strict=True)
+ except (LastNodeError, AttributeError):
+ docstring = None
+
+ type_alias = TypeAlias(
+ name=name,
+ value=value,
+ lineno=node.lineno,
+ endlineno=node.end_lineno,
+ type_parameters=TypeParameters(*self._get_type_parameters(node, scope=name)),
+ docstring=docstring,
+ parent=self.current,
+ )
+ self.current.set_member(name, type_alias)
+ self.extensions.call("on_instance", node=node, obj=type_alias, agent=self)
+ self.extensions.call("on_type_alias_instance", node=node, type_alias=type_alias, agent=self)
+
def visit_import(self, node: ast.Import) -> None:
"""Visit an import node.
diff --git a/src/_griffe/docstrings/google.py b/src/_griffe/docstrings/google.py
index 344af64e..241f03e5 100644
--- a/src/_griffe/docstrings/google.py
+++ b/src/_griffe/docstrings/google.py
@@ -28,8 +28,12 @@
DocstringSectionReceives,
DocstringSectionReturns,
DocstringSectionText,
+ DocstringSectionTypeAliases,
+ DocstringSectionTypeParameters,
DocstringSectionWarns,
DocstringSectionYields,
+ DocstringTypeAlias,
+ DocstringTypeParameter,
DocstringWarn,
DocstringYield,
)
@@ -55,6 +59,10 @@
"other arguments": DocstringSectionKind.other_parameters,
"other params": DocstringSectionKind.other_parameters,
"other parameters": DocstringSectionKind.other_parameters,
+ "type args": DocstringSectionKind.type_parameters,
+ "type arguments": DocstringSectionKind.type_parameters,
+ "type params": DocstringSectionKind.type_parameters,
+ "type parameters": DocstringSectionKind.type_parameters,
"raises": DocstringSectionKind.raises,
"exceptions": DocstringSectionKind.raises,
"returns": DocstringSectionKind.returns,
@@ -65,6 +73,7 @@
"functions": DocstringSectionKind.functions,
"methods": DocstringSectionKind.functions,
"classes": DocstringSectionKind.classes,
+ "type aliases": DocstringSectionKind.type_aliases,
"modules": DocstringSectionKind.modules,
"warns": DocstringSectionKind.warns,
"warnings": DocstringSectionKind.warns,
@@ -263,6 +272,75 @@ def _read_other_parameters_section(
return DocstringSectionOtherParameters(parameters), new_offset
+def _read_type_parameters_section(
+ docstring: Docstring,
+ *,
+ offset: int,
+ warn_unknown_params: bool = True,
+ **options: Any,
+) -> tuple[DocstringSectionTypeParameters | None, int]:
+ type_parameters = []
+ bound: str | Expr | None
+
+ block, new_offset = _read_block_items(docstring, offset=offset, **options)
+
+ for line_number, type_param_lines in block:
+ # check the presence of a name and description, separated by a colon
+ try:
+ name_with_bound, description = type_param_lines[0].split(":", 1)
+ except ValueError:
+ docstring_warning(
+ docstring,
+ line_number,
+ f"Failed to get 'name: description' pair from '{type_param_lines[0]}'",
+ )
+ continue
+
+ description = "\n".join([description.lstrip(), *type_param_lines[1:]]).rstrip("\n")
+
+ # use the type given after the type parameter name, if any
+ if " " in name_with_bound:
+ name, bound = name_with_bound.split(" ", 1)
+ if bound.startswith("(") and bound.endswith(")"):
+ bound = bound[1:-1]
+ # try to compile the annotation to transform it into an expression
+ bound = parse_docstring_annotation(bound, docstring)
+ else:
+ name = name_with_bound
+ # try to use the annotation from the signature
+ try:
+ bound = docstring.parent.type_parameters[name].annotation # type: ignore[union-attr]
+ except (AttributeError, KeyError):
+ bound = None
+
+ try:
+ default = docstring.parent.type_parameters[name].default # type: ignore[union-attr]
+ except (AttributeError, KeyError):
+ default = None
+
+ if warn_unknown_params:
+ with suppress(AttributeError): # for type parameters sections in objects without type parameters
+ type_params = docstring.parent.type_parameters # type: ignore[union-attr]
+ if name not in type_params:
+ message = f"Type parameter '{name}' does not appear in the {docstring.parent.kind.value} signature" # type: ignore[union-attr]
+ for starred_name in (f"*{name}", f"**{name}"):
+ if starred_name in type_params:
+ message += f". Did you mean '{starred_name}'?"
+ break
+ docstring_warning(docstring, line_number, message)
+
+ type_parameters.append(
+ DocstringTypeParameter(
+ name=name,
+ value=default,
+ annotation=bound,
+ description=description,
+ ),
+ )
+
+ return DocstringSectionTypeParameters(type_parameters), new_offset
+
+
def _read_attributes_section(
docstring: Docstring,
*,
@@ -379,6 +457,31 @@ def _read_classes_section(
return DocstringSectionClasses(classes), new_offset
+def _read_type_aliases_section(
+ docstring: Docstring,
+ *,
+ offset: int,
+ **options: Any,
+) -> tuple[DocstringSectionTypeAliases | None, int]:
+ type_aliases = []
+ block, new_offset = _read_block_items(docstring, offset=offset, **options)
+
+ for line_number, type_alias_lines in block:
+ try:
+ name, description = type_alias_lines[0].split(":", 1)
+ except ValueError:
+ docstring_warning(
+ docstring,
+ line_number,
+ f"Failed to get 'name: description' pair from '{type_alias_lines[0]}'",
+ )
+ continue
+ description = "\n".join([description.lstrip(), *type_alias_lines[1:]]).rstrip("\n")
+ type_aliases.append(DocstringTypeAlias(name=name, description=description))
+
+ return DocstringSectionTypeAliases(type_aliases), new_offset
+
+
def _read_modules_section(
docstring: Docstring,
*,
@@ -730,12 +833,14 @@ def _is_empty_line(line: str) -> bool:
_section_reader = {
DocstringSectionKind.parameters: _read_parameters_section,
DocstringSectionKind.other_parameters: _read_other_parameters_section,
+ DocstringSectionKind.type_parameters: _read_type_parameters_section,
DocstringSectionKind.raises: _read_raises_section,
DocstringSectionKind.warns: _read_warns_section,
DocstringSectionKind.examples: _read_examples_section,
DocstringSectionKind.attributes: _read_attributes_section,
DocstringSectionKind.functions: _read_functions_section,
DocstringSectionKind.classes: _read_classes_section,
+ DocstringSectionKind.type_aliases: _read_type_aliases_section,
DocstringSectionKind.modules: _read_modules_section,
DocstringSectionKind.returns: _read_returns_section,
DocstringSectionKind.yields: _read_yields_section,
diff --git a/src/_griffe/docstrings/models.py b/src/_griffe/docstrings/models.py
index 3aeadbe8..2d6e2728 100644
--- a/src/_griffe/docstrings/models.py
+++ b/src/_griffe/docstrings/models.py
@@ -5,8 +5,10 @@
from typing import TYPE_CHECKING
from _griffe.enumerations import DocstringSectionKind
+from _griffe.expressions import ExprTuple
if TYPE_CHECKING:
+ from collections.abc import Sequence
from typing import Any, Literal
from _griffe.expressions import Expr
@@ -52,7 +54,7 @@ def __init__(
*,
description: str,
annotation: str | Expr | None = None,
- value: str | None = None,
+ value: str | Expr | None = None,
) -> None:
"""Initialize the element.
@@ -65,7 +67,7 @@ def __init__(
super().__init__(description=description, annotation=annotation)
self.name: str = name
"""The element name."""
- self.value: str | None = value
+ self.value: str | Expr | None = value
"""The element value, if any"""
def as_dict(self, **kwargs: Any) -> dict[str, Any]:
@@ -142,7 +144,7 @@ class DocstringParameter(DocstringNamedElement):
"""This class represent a documented function parameter."""
@property
- def default(self) -> str | None:
+ def default(self) -> str | Expr | None:
"""The default value of this parameter."""
return self.value
@@ -151,6 +153,44 @@ def default(self, value: str) -> None:
self.value = value
+class DocstringTypeParameter(DocstringNamedElement):
+ """This class represent a documented type parameter."""
+
+ @property
+ def default(self) -> str | Expr | None:
+ """The default value of this type parameter."""
+ return self.value
+
+ @default.setter
+ def default(self, value: str) -> None:
+ self.value = value
+
+ @property
+ def bound(self) -> str | Expr | None:
+ """The bound of this type parameter."""
+ if not isinstance(self.annotation, ExprTuple):
+ return self.annotation
+ return None
+
+ @bound.setter
+ def bound(self, bound: str | Expr | None) -> None:
+ self.annotation = bound
+
+ @property
+ def constraints(self) -> tuple[str | Expr, ...] | None:
+ """The constraints of this type parameter."""
+ if isinstance(self.annotation, ExprTuple):
+ return tuple(self.annotation.elements)
+ return None
+
+ @constraints.setter
+ def constraints(self, constraints: Sequence[str | Expr] | None) -> None:
+ if constraints is not None:
+ self.annotation = ExprTuple(constraints)
+ else:
+ self.annotation = None
+
+
class DocstringAttribute(DocstringNamedElement):
"""This class represents a documented module/class attribute."""
@@ -173,6 +213,10 @@ def signature(self) -> str | Expr | None:
return self.annotation
+class DocstringTypeAlias(DocstringNamedElement):
+ """This class represents a documented type alias."""
+
+
class DocstringModule(DocstringNamedElement):
"""This class represents a documented module."""
@@ -256,6 +300,22 @@ class DocstringSectionOtherParameters(DocstringSectionParameters):
kind: DocstringSectionKind = DocstringSectionKind.other_parameters
+class DocstringSectionTypeParameters(DocstringSection):
+ """This class represents a type parameters section."""
+
+ kind: DocstringSectionKind = DocstringSectionKind.type_parameters
+
+ def __init__(self, value: list[DocstringTypeParameter], title: str | None = None) -> None:
+ """Initialize the section.
+
+ Parameters:
+ value: The section type parameters.
+ title: An optional title.
+ """
+ super().__init__(title)
+ self.value: list[DocstringTypeParameter] = value
+
+
class DocstringSectionRaises(DocstringSection):
"""This class represents a raises section."""
@@ -404,6 +464,22 @@ def __init__(self, value: list[DocstringClass], title: str | None = None) -> Non
self.value: list[DocstringClass] = value
+class DocstringSectionTypeAliases(DocstringSection):
+ """This class represents a type aliases section."""
+
+ kind: DocstringSectionKind = DocstringSectionKind.type_aliases
+
+ def __init__(self, value: list[DocstringTypeAlias], title: str | None = None) -> None:
+ """Initialize the section.
+
+ Parameters:
+ value: The section classes.
+ title: An optional title.
+ """
+ super().__init__(title)
+ self.value: list[DocstringTypeAlias] = value
+
+
class DocstringSectionModules(DocstringSection):
"""This class represents a modules section."""
diff --git a/src/_griffe/docstrings/numpy.py b/src/_griffe/docstrings/numpy.py
index 67505b40..c3aadfc2 100644
--- a/src/_griffe/docstrings/numpy.py
+++ b/src/_griffe/docstrings/numpy.py
@@ -47,8 +47,12 @@
DocstringSectionReceives,
DocstringSectionReturns,
DocstringSectionText,
+ DocstringSectionTypeAliases,
+ DocstringSectionTypeParameters,
DocstringSectionWarns,
DocstringSectionYields,
+ DocstringTypeAlias,
+ DocstringTypeParameter,
DocstringWarn,
DocstringYield,
)
@@ -68,6 +72,7 @@
"deprecated": DocstringSectionKind.deprecated,
"parameters": DocstringSectionKind.parameters,
"other parameters": DocstringSectionKind.other_parameters,
+ "type parameters": DocstringSectionKind.type_parameters,
"returns": DocstringSectionKind.returns,
"yields": DocstringSectionKind.yields,
"receives": DocstringSectionKind.receives,
@@ -78,6 +83,7 @@
"functions": DocstringSectionKind.functions,
"methods": DocstringSectionKind.functions,
"classes": DocstringSectionKind.classes,
+ "type aliases": DocstringSectionKind.type_aliases,
"modules": DocstringSectionKind.modules,
}
@@ -324,6 +330,76 @@ def _read_other_parameters_section(
return None, new_offset
+def _read_type_parameters_section(
+ docstring: Docstring,
+ *,
+ offset: int,
+ warn_unknown_params: bool = True,
+ **options: Any,
+) -> tuple[DocstringSectionTypeParameters | None, int]:
+ type_parameters: list[DocstringTypeParameter] = []
+ bound: str | Expr | None
+
+ items, new_offset = _read_block_items(docstring, offset=offset, **options)
+
+ for item in items:
+ match = _RE_PARAMETER.match(item[0])
+ if not match:
+ docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'")
+ continue
+
+ names = match.group("names").split(", ")
+ bound = match.group("type") or None
+ choices = match.group("choices")
+ default = None
+ if choices:
+ bound = choices
+ default = choices.split(", ", 1)[0]
+ elif bound:
+ match = re.match(r"^(?P.+),\s+default(?: |: |=)(?P.+)$", bound)
+ if match:
+ default = match.group("default")
+ bound = match.group("annotation")
+ description = "\n".join(item[1:]).rstrip() if len(item) > 1 else ""
+
+ if bound is None:
+ # try to use the bound from the signature
+ for name in names:
+ with suppress(AttributeError, KeyError):
+ bound = docstring.parent.type_parameters[name].annotation # type: ignore[union-attr]
+ break
+ else:
+ bound = parse_docstring_annotation(bound, docstring, log_level=LogLevel.debug)
+
+ if default is None:
+ for name in names:
+ with suppress(AttributeError, KeyError):
+ default = docstring.parent.type_parameters[name].default # type: ignore[union-attr]
+ break
+
+ if warn_unknown_params:
+ with suppress(AttributeError): # for parameters sections in objects without parameters
+ type_params = docstring.parent.type_parameters # type: ignore[union-attr]
+ for name in names:
+ if name not in type_params:
+ message = f"Type parameter '{name}' does not appear in the {docstring.parent.kind} signature" # type: ignore[union-attr]
+ for starred_name in (f"*{name}", f"**{name}"):
+ if starred_name in type_params:
+ message += f". Did you mean '{starred_name}'?"
+ break
+ docstring_warning(docstring, new_offset, message)
+
+ type_parameters.extend(
+ DocstringTypeParameter(name, value=default, annotation=bound, description=description) for name in names
+ )
+
+ if type_parameters:
+ return DocstringSectionTypeParameters(type_parameters), new_offset
+
+ docstring_warning(docstring, new_offset, f"Empty type parameters section at line {offset}")
+ return None, new_offset
+
+
def _read_deprecated_section(
docstring: Docstring,
*,
@@ -660,6 +736,26 @@ def _read_classes_section(
return DocstringSectionClasses(classes), new_offset
+def _read_type_aliases_section(
+ docstring: Docstring,
+ *,
+ offset: int,
+ **options: Any,
+) -> tuple[DocstringSectionTypeAliases | None, int]:
+ items, new_offset = _read_block_items(docstring, offset=offset, **options)
+
+ if not items:
+ docstring_warning(docstring, new_offset, f"Empty type aliases section at line {offset}")
+ return None, new_offset
+
+ type_aliases = []
+ for item in items:
+ name = item[0]
+ text = dedent("\n".join(item[1:])).strip()
+ type_aliases.append(DocstringTypeAlias(name=name, description=text))
+ return DocstringSectionTypeAliases(type_aliases), new_offset
+
+
def _read_modules_section(
docstring: Docstring,
*,
@@ -779,6 +875,7 @@ def _append_section(sections: list, current: list[str], admonition_title: str) -
_section_reader = {
DocstringSectionKind.parameters: _read_parameters_section,
DocstringSectionKind.other_parameters: _read_other_parameters_section,
+ DocstringSectionKind.type_parameters: _read_type_parameters_section,
DocstringSectionKind.deprecated: _read_deprecated_section,
DocstringSectionKind.raises: _read_raises_section,
DocstringSectionKind.warns: _read_warns_section,
@@ -786,6 +883,7 @@ def _append_section(sections: list, current: list[str], admonition_title: str) -
DocstringSectionKind.attributes: _read_attributes_section,
DocstringSectionKind.functions: _read_functions_section,
DocstringSectionKind.classes: _read_classes_section,
+ DocstringSectionKind.type_aliases: _read_type_aliases_section,
DocstringSectionKind.modules: _read_modules_section,
DocstringSectionKind.returns: _read_returns_section,
DocstringSectionKind.yields: _read_yields_section,
diff --git a/src/_griffe/encoders.py b/src/_griffe/encoders.py
index 5cda844f..d4329020 100644
--- a/src/_griffe/encoders.py
+++ b/src/_griffe/encoders.py
@@ -8,7 +8,7 @@
from typing import Any, Callable
from _griffe import expressions
-from _griffe.enumerations import Kind, ParameterKind
+from _griffe.enumerations import Kind, ParameterKind, TypeParameterKind
from _griffe.models import (
Alias,
Attribute,
@@ -20,6 +20,9 @@
Object,
Parameter,
Parameters,
+ TypeAlias,
+ TypeParameter,
+ TypeParameters,
)
_json_encoder_map: dict[type, Callable[[Any], Any]] = {
@@ -122,6 +125,15 @@ def _load_parameter(obj_dict: dict[str, Any]) -> Parameter:
)
+def _load_type_parameter(obj_dict: dict[str, Any]) -> TypeParameter:
+ return TypeParameter(
+ obj_dict["name"],
+ kind=TypeParameterKind(obj_dict["kind"]),
+ bound=obj_dict["annotation"],
+ default=obj_dict["default"],
+ )
+
+
def _attach_parent_to_expr(expr: expressions.Expr | str | None, parent: Module | Class) -> None:
if not isinstance(expr, expressions.Expr):
return
@@ -132,7 +144,7 @@ def _attach_parent_to_expr(expr: expressions.Expr | str | None, parent: Module |
elem.first.parent = parent
-def _attach_parent_to_exprs(obj: Class | Function | Attribute, parent: Module | Class) -> None:
+def _attach_parent_to_exprs(obj: Class | Function | Attribute | TypeAlias, parent: Module | Class) -> None:
# Every name and attribute expression must be reattached
# to its parent Griffe object (using its `parent` attribute),
# to allow resolving names.
@@ -141,11 +153,17 @@ def _attach_parent_to_exprs(obj: Class | Function | Attribute, parent: Module |
_attach_parent_to_expr(obj.docstring.value, parent)
for decorator in obj.decorators:
_attach_parent_to_expr(decorator.value, parent)
+ for type_parameter in obj.type_parameters:
+ _attach_parent_to_expr(type_parameter.annotation, parent)
+ _attach_parent_to_expr(type_parameter.default, parent)
elif isinstance(obj, Function):
if obj.docstring:
_attach_parent_to_expr(obj.docstring.value, parent)
for decorator in obj.decorators:
_attach_parent_to_expr(decorator.value, parent)
+ for type_parameter in obj.type_parameters:
+ _attach_parent_to_expr(type_parameter.annotation, parent)
+ _attach_parent_to_expr(type_parameter.default, parent)
for param in obj.parameters:
_attach_parent_to_expr(param.annotation, parent)
_attach_parent_to_expr(param.default, parent)
@@ -154,6 +172,13 @@ def _attach_parent_to_exprs(obj: Class | Function | Attribute, parent: Module |
if obj.docstring:
_attach_parent_to_expr(obj.docstring.value, parent)
_attach_parent_to_expr(obj.value, parent)
+ elif isinstance(obj, TypeAlias):
+ if obj.docstring:
+ _attach_parent_to_expr(obj.docstring.value, parent)
+ for type_parameter in obj.type_parameters:
+ _attach_parent_to_expr(type_parameter.annotation, parent)
+ _attach_parent_to_expr(type_parameter.default, parent)
+ _attach_parent_to_expr(obj.value, parent)
def _load_module(obj_dict: dict[str, Any]) -> Module:
@@ -178,6 +203,7 @@ def _load_class(obj_dict: dict[str, Any]) -> Class:
endlineno=obj_dict.get("endlineno"),
docstring=_load_docstring(obj_dict),
decorators=_load_decorators(obj_dict),
+ type_parameters=TypeParameters(*obj_dict["type_parameters"]),
bases=obj_dict["bases"],
)
# YORE: Bump 2: Replace line with `members = obj_dict.get("members", {}).values()`.
@@ -200,6 +226,7 @@ def _load_function(obj_dict: dict[str, Any]) -> Function:
parameters=Parameters(*obj_dict["parameters"]),
returns=obj_dict["returns"],
decorators=_load_decorators(obj_dict),
+ type_parameters=TypeParameters(*obj_dict["type_parameters"]),
lineno=obj_dict["lineno"],
endlineno=obj_dict.get("endlineno"),
docstring=_load_docstring(obj_dict),
@@ -230,16 +257,30 @@ def _load_alias(obj_dict: dict[str, Any]) -> Alias:
)
-_loader_map: dict[Kind, Callable[[dict[str, Any]], Module | Class | Function | Attribute | Alias]] = {
+def _load_type_alias(obj_dict: dict[str, Any]) -> TypeAlias:
+ return TypeAlias(
+ name=obj_dict["name"],
+ value=obj_dict["value"],
+ type_parameters=TypeParameters(*obj_dict["type_parameters"]),
+ lineno=obj_dict["lineno"],
+ endlineno=obj_dict.get("endlineno"),
+ docstring=_load_docstring(obj_dict),
+ )
+
+
+_loader_map: dict[Kind, Callable[[dict[str, Any]], Object | Alias]] = {
Kind.MODULE: _load_module,
Kind.CLASS: _load_class,
Kind.FUNCTION: _load_function,
Kind.ATTRIBUTE: _load_attribute,
Kind.ALIAS: _load_alias,
+ Kind.TYPE_ALIAS: _load_type_alias,
}
-def json_decoder(obj_dict: dict[str, Any]) -> dict[str, Any] | Object | Alias | Parameter | str | expressions.Expr:
+def json_decoder(
+ obj_dict: dict[str, Any],
+) -> dict[str, Any] | Object | Alias | Parameter | TypeParameter | str | expressions.Expr:
"""Decode dictionaries as data classes.
The [`json.loads`][] method walks the tree from bottom to top.
@@ -261,11 +302,15 @@ def json_decoder(obj_dict: dict[str, Any]) -> dict[str, Any] | Object | Alias |
# Load objects and parameters.
if "kind" in obj_dict:
- try:
- kind = Kind(obj_dict["kind"])
- except ValueError:
+ kind = obj_dict["kind"]
+ if kind in _loader_map:
+ return _loader_map[kind](obj_dict)
+ # YORE: EOL 3.11: Replace `.__members__.values()` with `` within line.
+ if kind in ParameterKind.__members__.values():
return _load_parameter(obj_dict)
- return _loader_map[kind](obj_dict)
+ # YORE: EOL 3.11: Replace `.__members__.values()` with `` within line.
+ if kind in TypeParameterKind.__members__.values():
+ return _load_type_parameter(obj_dict)
# Return dict as is.
return obj_dict
diff --git a/src/_griffe/enumerations.py b/src/_griffe/enumerations.py
index 7c204c55..015c9152 100644
--- a/src/_griffe/enumerations.py
+++ b/src/_griffe/enumerations.py
@@ -33,6 +33,8 @@ class DocstringSectionKind(str, Enum):
"""Parameters section."""
other_parameters = "other parameters"
"""Other parameters (keyword arguments) section."""
+ type_parameters = "type parameters"
+ """Type parameters section."""
raises = "raises"
"""Raises (exceptions) section."""
warns = "warns"
@@ -51,6 +53,8 @@ class DocstringSectionKind(str, Enum):
"""Functions section."""
classes = "classes"
"""Classes section."""
+ type_aliases = "type aliases"
+ """Type aliases section."""
modules = "modules"
"""Modules section."""
deprecated = "deprecated"
@@ -74,6 +78,17 @@ class ParameterKind(str, Enum):
"""Variadic keyword parameter."""
+class TypeParameterKind(str, Enum):
+ """Enumeration of the different type parameter kinds."""
+
+ type_var = "type-var"
+ """Type variable."""
+ type_var_tuple = "type-var-tuple"
+ """Type variable tuple."""
+ param_spec = "param-spec"
+ """Parameter specification variable."""
+
+
class Kind(str, Enum):
"""Enumeration of the different object kinds."""
@@ -87,6 +102,8 @@ class Kind(str, Enum):
"""Attributes and properties."""
ALIAS = "alias"
"""Aliases (imported objects)."""
+ TYPE_ALIAS = "type alias"
+ """Type aliases."""
class ExplanationStyle(str, Enum):
@@ -177,6 +194,8 @@ class ObjectKind(str, Enum):
"""Get/set descriptors."""
PROPERTY = "property"
"""Properties."""
+ TYPE_ALIAS = "type_alias"
+ """Type aliases."""
ATTRIBUTE = "attribute"
"""Attributes."""
diff --git a/src/_griffe/expressions.py b/src/_griffe/expressions.py
index 80c6c02a..fdadcda8 100644
--- a/src/_griffe/expressions.py
+++ b/src/_griffe/expressions.py
@@ -704,6 +704,8 @@ class ExprName(Expr): # noqa: PLW1641
"""Actual name."""
parent: str | ExprName | Module | Class | Function | None = None
"""Parent (for resolution in its scope)."""
+ member: str | None = None
+ """Member name (for resolution in its scope)."""
def __eq__(self, other: object) -> bool:
"""Two name expressions are equal if they have the same `name` value (`parent` is ignored)."""
@@ -735,8 +737,9 @@ def canonical_path(self) -> str:
return f"{self.parent.canonical_path}.{self.name}"
if isinstance(self.parent, str):
return f"{self.parent}.{self.name}"
+ parent = self.parent.members.get(self.member, self.parent) # type: ignore[arg-type]
try:
- return self.parent.resolve(self.name)
+ return parent.resolve(self.name)
except NameResolutionError:
return self.name
@@ -776,6 +779,11 @@ def is_enum_value(self) -> bool:
except Exception: # noqa: BLE001
return False
+ @property
+ def is_type_parameter(self) -> bool:
+ """Whether this name resolves to a type parameter."""
+ return "[" in self.canonical_path
+
# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
@dataclass(eq=True, **_dataclass_opts)
@@ -1232,8 +1240,8 @@ def _build_listcomp(node: ast.ListComp, parent: Module | Class, **kwargs: Any) -
return ExprListComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators])
-def _build_name(node: ast.Name, parent: Module | Class, **kwargs: Any) -> Expr: # noqa: ARG001
- return ExprName(node.id, parent)
+def _build_name(node: ast.Name, parent: Module | Class, member: str | None = None, **kwargs: Any) -> Expr: # noqa: ARG001
+ return ExprName(node.id, parent, member)
def _build_named_expr(node: ast.NamedExpr, parent: Module | Class, **kwargs: Any) -> Expr:
@@ -1343,7 +1351,7 @@ def _build_yield_from(node: ast.YieldFrom, parent: Module | Class, **kwargs: Any
}
-def _build(node: ast.AST, parent: Module | Class, **kwargs: Any) -> Expr:
+def _build(node: ast.AST, parent: Module | Class, /, **kwargs: Any) -> Expr:
return _node_map[type(node)](node, parent, **kwargs)
@@ -1351,6 +1359,7 @@ def get_expression(
node: ast.AST | None,
parent: Module | Class,
*,
+ member: str | None = None,
parse_strings: bool | None = None,
) -> Expr | None:
"""Build an expression from an AST.
@@ -1358,6 +1367,7 @@ def get_expression(
Parameters:
node: The annotation node.
parent: The parent used to resolve the name.
+ member: The member name (for resolution in its scope).
parse_strings: Whether to try and parse strings as type annotations.
Returns:
@@ -1372,13 +1382,14 @@ def get_expression(
parse_strings = False
else:
parse_strings = not module.imports_future_annotations
- return _build(node, parent, parse_strings=parse_strings)
+ return _build(node, parent, member=member, parse_strings=parse_strings)
def safe_get_expression(
node: ast.AST | None,
parent: Module | Class,
*,
+ member: str | None = None,
parse_strings: bool | None = None,
log_level: LogLevel | None = LogLevel.error,
msg_format: str = "{path}:{lineno}: Failed to get expression from {node_class}: {error}",
@@ -1388,6 +1399,7 @@ def safe_get_expression(
Parameters:
node: The annotation node.
parent: The parent used to resolve the name.
+ member: The member name (for resolution in its scope).
parse_strings: Whether to try and parse strings as type annotations.
log_level: Log level to use to log a message. None to disable logging.
msg_format: A format string for the log message. Available placeholders:
@@ -1397,7 +1409,7 @@ def safe_get_expression(
A string or resovable name or expression.
"""
try:
- return get_expression(node, parent, parse_strings=parse_strings)
+ return get_expression(node, parent, member=member, parse_strings=parse_strings)
except Exception as error: # noqa: BLE001
if log_level is None:
return None
diff --git a/src/_griffe/extensions/base.py b/src/_griffe/extensions/base.py
index 0ef6fe8c..2c764d68 100644
--- a/src/_griffe/extensions/base.py
+++ b/src/_griffe/extensions/base.py
@@ -22,7 +22,7 @@
from _griffe.agents.nodes.runtime import ObjectNode
from _griffe.agents.visitor import Visitor
from _griffe.loader import GriffeLoader
- from _griffe.models import Alias, Attribute, Class, Function, Module, Object
+ from _griffe.models import Alias, Attribute, Class, Function, Module, Object, TypeAlias
class Extension:
@@ -235,6 +235,32 @@ def on_attribute_instance(
**kwargs: For forward-compatibility.
"""
+ def on_type_alias_node(self, *, node: ast.AST | ObjectNode, agent: Visitor | Inspector, **kwargs: Any) -> None:
+ """Run when visiting a new type alias node during static/dynamic analysis.
+
+ Parameters:
+ node: The currently visited node.
+ agent: The analysis agent currently running.
+ **kwargs: For forward-compatibility.
+ """
+
+ def on_type_alias_instance(
+ self,
+ *,
+ node: ast.AST | ObjectNode,
+ type_alias: TypeAlias,
+ agent: Visitor | Inspector,
+ **kwargs: Any,
+ ) -> None:
+ """Run when a TypeAlias has been created.
+
+ Parameters:
+ node: The currently visited node.
+ type_alias: The type alias instance.
+ agent: The analysis agent currently running.
+ **kwargs: For forward-compatibility.
+ """
+
def on_alias(
self,
*,
diff --git a/src/_griffe/importer.py b/src/_griffe/importer.py
index 3b5b02aa..b75b44ee 100644
--- a/src/_griffe/importer.py
+++ b/src/_griffe/importer.py
@@ -43,7 +43,7 @@ def sys_path(*paths: str | Path) -> Iterator[None]:
def dynamic_import(import_path: str, import_paths: Sequence[str | Path] | None = None) -> Any:
"""Dynamically import the specified object.
- It can be a module, class, method, function, attribute,
+ It can be a module, class, method, function, attribute, type alias,
nested arbitrarily.
It works like this:
diff --git a/src/_griffe/loader.py b/src/_griffe/loader.py
index f439c305..256bbce4 100644
--- a/src/_griffe/loader.py
+++ b/src/_griffe/loader.py
@@ -742,8 +742,8 @@ def load(
The extracted information is stored in a collection of modules, which can be queried later.
Each collected module is a tree of objects, representing the structure of the module.
See the [`Module`][griffe.Module], [`Class`][griffe.Class],
- [`Function`][griffe.Function], and [`Attribute`][griffe.Attribute] classes
- for more information.
+ [`Function`][griffe.Function], [`Attribute`][griffe.Attribute], and
+ [`TypeAlias`][griffe.TypeAlias] classes for more information.
The main class used to load modules is [`GriffeLoader`][griffe.GriffeLoader].
Convenience functions like this one and [`load_git`][griffe.load_git] are also available.
diff --git a/src/_griffe/merger.py b/src/_griffe/merger.py
index 1f494d9a..ac991f93 100644
--- a/src/_griffe/merger.py
+++ b/src/_griffe/merger.py
@@ -9,7 +9,7 @@
from _griffe.logger import logger
if TYPE_CHECKING:
- from _griffe.models import Attribute, Class, Function, Module, Object
+ from _griffe.models import Attribute, Class, Function, Module, Object, TypeAlias
def _merge_module_stubs(module: Module, stubs: Module) -> None:
@@ -21,6 +21,7 @@ def _merge_module_stubs(module: Module, stubs: Module) -> None:
def _merge_class_stubs(class_: Class, stubs: Class) -> None:
_merge_stubs_docstring(class_, stubs)
_merge_stubs_overloads(class_, stubs)
+ _merge_stubs_type_parameters(class_, stubs)
_merge_stubs_members(class_, stubs)
@@ -30,6 +31,7 @@ def _merge_function_stubs(function: Function, stubs: Function) -> None:
with suppress(KeyError):
function.parameters[parameter.name].annotation = parameter.annotation
function.returns = stubs.returns
+ _merge_stubs_type_parameters(function, stubs)
def _merge_attribute_stubs(attribute: Attribute, stubs: Attribute) -> None:
@@ -39,11 +41,21 @@ def _merge_attribute_stubs(attribute: Attribute, stubs: Attribute) -> None:
attribute.value = stubs.value
+def _merge_type_alias_stubs(type_alias: TypeAlias, stubs: TypeAlias) -> None:
+ _merge_stubs_docstring(type_alias, stubs)
+ _merge_stubs_type_parameters(type_alias, stubs)
+
+
def _merge_stubs_docstring(obj: Object, stubs: Object) -> None:
if not obj.docstring and stubs.docstring:
obj.docstring = stubs.docstring
+def _merge_stubs_type_parameters(obj: Class | Function | TypeAlias, stubs: Class | Function | TypeAlias) -> None:
+ if not obj.type_parameters and stubs.type_parameters:
+ obj.type_parameters = stubs.type_parameters
+
+
def _merge_stubs_overloads(obj: Module | Class, stubs: Module | Class) -> None:
for function_name, overloads in list(stubs.overloads.items()):
if overloads:
@@ -88,6 +100,8 @@ def _merge_stubs_members(obj: Module | Class, stubs: Module | Class) -> None:
_merge_function_stubs(obj_member, stub_member) # type: ignore[arg-type]
elif obj_member.is_attribute:
_merge_attribute_stubs(obj_member, stub_member) # type: ignore[arg-type]
+ elif obj_member.is_type_alias:
+ _merge_type_alias_stubs(obj_member, stub_member) # type: ignore[arg-type]
else:
stub_member.runtime = False
obj.set_member(member_name, stub_member)
diff --git a/src/_griffe/mixins.py b/src/_griffe/mixins.py
index 2c21b116..e25423dc 100644
--- a/src/_griffe/mixins.py
+++ b/src/_griffe/mixins.py
@@ -14,7 +14,7 @@
if TYPE_CHECKING:
from collections.abc import Sequence
- from _griffe.models import Alias, Attribute, Class, Function, Module, Object
+ from _griffe.models import Alias, Attribute, Class, Function, Module, Object, TypeAlias
_ObjType = TypeVar("_ObjType")
@@ -279,6 +279,7 @@ class ObjectAliasMixin(GetMembersMixin, SetMembersMixin, DelMembersMixin, Serial
classes: The class members.
functions: The function members.
attributes: The attribute members.
+ type_aliases: The type alias members.
is_private: Whether this object/alias is private (starts with `_`) but not special.
is_class_private: Whether this object/alias is class-private (starts with `__` and is a class member).
is_special: Whether this object/alias is special ("dunder" attribute/method, starts and end with `__`).
@@ -336,6 +337,15 @@ def attributes(self) -> dict[str, Attribute]:
"""
return {name: member for name, member in self.all_members.items() if member.kind is Kind.ATTRIBUTE} # type: ignore[misc]
+ @property
+ def type_aliases(self) -> dict[str, TypeAlias]:
+ """The type alias members.
+
+ This method is part of the consumer API:
+ do not use when producing Griffe trees!
+ """
+ return {name: member for name, member in self.all_members.items() if member.kind is Kind.TYPE_ALIAS} # type: ignore[misc]
+
@property
def is_private(self) -> bool:
"""Whether this object/alias is private (starts with `_`) but not special."""
@@ -443,3 +453,8 @@ def is_deprecated(self) -> bool:
"""Whether this object is deprecated."""
# NOTE: We might want to add more ways to detect deprecations in the future.
return bool(self.deprecated) # type: ignore[attr-defined]
+
+ @property
+ def is_generic(self) -> bool:
+ """Whether this object is generic."""
+ return bool(self.type_parameters) # type: ignore[attr-defined]
diff --git a/src/_griffe/models.py b/src/_griffe/models.py
index 7a39d26e..bed1553c 100644
--- a/src/_griffe/models.py
+++ b/src/_griffe/models.py
@@ -12,9 +12,9 @@
from _griffe.c3linear import c3linear_merge
from _griffe.docstrings.parsers import DocstringStyle, parse
-from _griffe.enumerations import Kind, ParameterKind, Parser
+from _griffe.enumerations import Kind, ParameterKind, Parser, TypeParameterKind
from _griffe.exceptions import AliasResolutionError, BuiltinModuleError, CyclicAliasError, NameResolutionError
-from _griffe.expressions import ExprCall, ExprName
+from _griffe.expressions import ExprCall, ExprName, ExprTuple
from _griffe.logger import logger
from _griffe.mixins import ObjectAliasMixin
@@ -25,6 +25,7 @@
from _griffe.docstrings.models import DocstringSection
from _griffe.expressions import Expr
+
from functools import cached_property
@@ -370,6 +371,186 @@ def add(self, parameter: Parameter) -> None:
self._params.append(parameter)
+class TypeParameter:
+ """This class represents a type parameter."""
+
+ def __init__(
+ self,
+ name: str,
+ *,
+ kind: TypeParameterKind,
+ bound: str | Expr | None = None,
+ constraints: Sequence[str | Expr] | None = None,
+ default: str | Expr | None = None,
+ ) -> None:
+ """Initialize the type parameter.
+
+ Parameters:
+ name: The type parameter name, without leading stars (`*` or `**`).
+ kind: The type parameter kind.
+ bound: The type parameter bound, if any.
+ Mutually exclusive with `constraints`.
+ constraints: The type parameter constraints, if any.
+ Mutually exclusive with `bound`.
+ default: The type parameter default, if any.
+
+ Raises:
+ ValueError: When more than one of `bound` and `constraints` is set.
+ """
+ if bound is not None and constraints:
+ raise ValueError("bound and constraints are mutually exclusive")
+
+ self.name: str = name
+ """The type parameter name."""
+
+ self.kind: TypeParameterKind = kind
+ """The type parameter kind."""
+
+ self.annotation: str | Expr | None
+ """The type parameter bound or constraints."""
+
+ if constraints:
+ self.constraints = constraints
+ else:
+ self.bound = bound
+
+ self.default: str | Expr | None = default
+ """The type parameter default value."""
+
+ def __repr__(self) -> str:
+ return f"TypeParameter(name={self.name!r}, kind={self.kind!r}, bound={self.annotation!r}, default={self.default!r})"
+
+ @property
+ def bound(self) -> str | Expr | None:
+ """The type parameter bound."""
+ if not isinstance(self.annotation, ExprTuple):
+ return self.annotation
+ return None
+
+ @bound.setter
+ def bound(self, bound: str | Expr | None) -> None:
+ self.annotation = bound
+
+ @property
+ def constraints(self) -> tuple[str | Expr, ...] | None:
+ """The type parameter constraints."""
+ if isinstance(self.annotation, ExprTuple):
+ return tuple(self.annotation.elements)
+ return None
+
+ @constraints.setter
+ def constraints(self, constraints: Sequence[str | Expr] | None) -> None:
+ if constraints is not None:
+ self.annotation = ExprTuple(constraints)
+ else:
+ self.annotation = None
+
+ def as_dict(self, **kwargs: Any) -> dict[str, Any]: # noqa: ARG002
+ """Return this type parameter's data as a dictionary.
+
+ Parameters:
+ **kwargs: Additional serialization options.
+
+ Returns:
+ A dictionary.
+ """
+ base: dict[str, Any] = {
+ "name": self.name,
+ "kind": self.kind,
+ "annotation": self.annotation,
+ "default": self.default,
+ }
+ return base
+
+
+class TypeParameters:
+ """This class is a container for type parameters.
+
+ It allows to get type parameters using their position (index) or their name:
+
+ ```pycon
+ >>> type_parameters = TypeParameters(TypeParameter("hello"), kind=TypeParameterKind.type_var)
+ >>> type_parameters[0] is type_parameters["hello"]
+ True
+ ```
+ """
+
+ def __init__(self, *type_parameters: TypeParameter) -> None:
+ """Initialize the type parameters container.
+
+ Parameters:
+ *type_parameters: The initial type parameters to add to the container.
+ """
+ self._type_params: list[TypeParameter] = list(type_parameters)
+
+ def __repr__(self) -> str:
+ return f"TypeParameters({', '.join(repr(type_param) for type_param in self._type_params)})"
+
+ def __getitem__(self, name_or_index: int | str) -> TypeParameter:
+ """Get a type parameter by index or name."""
+ if isinstance(name_or_index, int):
+ return self._type_params[name_or_index]
+ name = name_or_index.lstrip("*")
+ try:
+ return next(param for param in self._type_params if param.name == name)
+ except StopIteration as error:
+ raise KeyError(f"type parameter {name_or_index} not found") from error
+
+ def __setitem__(self, name_or_index: int | str, type_parameter: TypeParameter) -> None:
+ """Set a type parameter by index or name."""
+ if isinstance(name_or_index, int):
+ self._type_params[name_or_index] = type_parameter
+ else:
+ name = name_or_index.lstrip("*")
+ try:
+ index = next(idx for idx, param in enumerate(self._type_params) if param.name == name)
+ except StopIteration:
+ self._type_params.append(type_parameter)
+ else:
+ self._type_params[index] = type_parameter
+
+ def __delitem__(self, name_or_index: int | str) -> None:
+ """Delete a type parameter by index or name."""
+ if isinstance(name_or_index, int):
+ del self._type_params[name_or_index]
+ else:
+ name = name_or_index.lstrip("*")
+ try:
+ index = next(idx for idx, param in enumerate(self._type_params) if param.name == name)
+ except StopIteration as error:
+ raise KeyError(f"type parameter {name_or_index} not found") from error
+ del self._type_params[index]
+
+ def __len__(self):
+ """The number of type parameters."""
+ return len(self._type_params)
+
+ def __iter__(self):
+ """Iterate over the type parameters, in order."""
+ return iter(self._type_params)
+
+ def __contains__(self, type_param_name: str):
+ """Whether a type parameter with the given name is present."""
+ try:
+ next(param for param in self._type_params if param.name == type_param_name.lstrip("*"))
+ except StopIteration:
+ return False
+ return True
+
+ def add(self, type_parameter: TypeParameter) -> None:
+ """Add a type parameter to the container.
+
+ Parameters:
+ type_parameter: The function parameter to add.
+
+ Raises:
+ ValueError: When a type parameter with the same name is already present.
+ """
+ if type_parameter.name in self:
+ raise ValueError(f"type parameter {type_parameter.name} already present")
+ self._type_params.append(type_parameter)
+
+
class Object(ObjectAliasMixin):
"""An abstract class representing a Python object."""
@@ -393,6 +574,7 @@ def __init__(
endlineno: int | None = None,
runtime: bool = True,
docstring: Docstring | None = None,
+ type_parameters: TypeParameters | None = None,
parent: Module | Class | None = None,
lines_collection: LinesCollection | None = None,
modules_collection: ModulesCollection | None = None,
@@ -405,6 +587,7 @@ def __init__(
endlineno: The object ending line (inclusive), or None for modules.
runtime: Whether this object is present at runtime or not.
docstring: The object docstring.
+ type_parameters: The object type parameters, if any.
parent: The object parent.
lines_collection: A collection of source code lines.
modules_collection: A collection of modules.
@@ -431,11 +614,14 @@ def __init__(
[`has_docstrings`][griffe.Object.has_docstrings].
"""
+ self.type_parameters: TypeParameters = type_parameters or TypeParameters()
+ """The object type parameters."""
+
self.parent: Module | Class | None = parent
"""The parent of the object (none if top module)."""
self.members: dict[str, Object | Alias] = {}
- """The object members (modules, classes, functions, attributes).
+ """The object members (modules, classes, functions, attributes, type aliases).
See also: [`inherited_members`][griffe.Object.inherited_members],
[`get_member`][griffe.Object.get_member],
@@ -554,6 +740,7 @@ def is_kind(self, kind: str | Kind | set[str | Kind]) -> bool:
[`is_class`][griffe.Object.is_class],
[`is_function`][griffe.Object.is_function],
[`is_attribute`][griffe.Object.is_attribute],
+ [`is_type_alias`][griffe.Object.is_type_alias],
[`is_alias`][griffe.Object.is_alias].
Parameters:
@@ -604,6 +791,7 @@ def is_module(self) -> bool:
[`is_class`][griffe.Object.is_class],
[`is_function`][griffe.Object.is_function],
[`is_attribute`][griffe.Object.is_attribute],
+ [`is_type_alias`][griffe.Object.is_type_alias],
[`is_alias`][griffe.Object.is_alias],
[`is_kind`][griffe.Object.is_kind].
"""
@@ -616,6 +804,7 @@ def is_class(self) -> bool:
See also: [`is_module`][griffe.Object.is_module].
[`is_function`][griffe.Object.is_function],
[`is_attribute`][griffe.Object.is_attribute],
+ [`is_type_alias`][griffe.Object.is_type_alias],
[`is_alias`][griffe.Object.is_alias],
[`is_kind`][griffe.Object.is_kind].
"""
@@ -628,6 +817,7 @@ def is_function(self) -> bool:
See also: [`is_module`][griffe.Object.is_module].
[`is_class`][griffe.Object.is_class],
[`is_attribute`][griffe.Object.is_attribute],
+ [`is_type_alias`][griffe.Object.is_type_alias],
[`is_alias`][griffe.Object.is_alias],
[`is_kind`][griffe.Object.is_kind].
"""
@@ -640,11 +830,25 @@ def is_attribute(self) -> bool:
See also: [`is_module`][griffe.Object.is_module].
[`is_class`][griffe.Object.is_class],
[`is_function`][griffe.Object.is_function],
+ [`is_type_alias`][griffe.Object.is_type_alias],
[`is_alias`][griffe.Object.is_alias],
[`is_kind`][griffe.Object.is_kind].
"""
return self.kind is Kind.ATTRIBUTE
+ @property
+ def is_type_alias(self) -> bool:
+ """Whether this object is a type alias.
+
+ See also: [`is_module`][griffe.Object.is_module].
+ [`is_class`][griffe.Object.is_class],
+ [`is_function`][griffe.Object.is_function],
+ [`is_attribute`][griffe.Object.is_attribute],
+ [`is_alias`][griffe.Object.is_alias],
+ [`is_kind`][griffe.Object.is_kind].
+ """
+ return self.kind is Kind.TYPE_ALIAS
+
@property
def is_init_module(self) -> bool:
"""Whether this object is an `__init__.py` module.
@@ -943,7 +1147,18 @@ def resolve(self, name: str) -> str:
# TODO: Better match Python's own scoping rules?
# Also, maybe return regular paths instead of canonical ones?
- # Name is a member this object.
+ # Name is a type parameter.
+ if name in self.type_parameters:
+ type_parameter = self.type_parameters[name]
+ if type_parameter.kind is TypeParameterKind.type_var_tuple:
+ prefix = "*"
+ elif type_parameter.kind is TypeParameterKind.param_spec:
+ prefix = "**"
+ else:
+ prefix = ""
+ return f"{self.path}[{prefix}{name}]"
+
+ # Name is a member of this object.
if name in self.members:
if self.members[name].is_alias:
return self.members[name].target_path # type: ignore[union-attr]
@@ -993,6 +1208,8 @@ def as_dict(self, *, full: bool = False, **kwargs: Any) -> dict[str, Any]:
base["endlineno"] = self.endlineno
if self.docstring:
base["docstring"] = self.docstring
+ if self.type_parameters:
+ base["type_parameters"] = [type_param.as_dict(**kwargs) for type_param in self.type_parameters]
base["labels"] = self.labels
base["members"] = {name: member.as_dict(full=full, **kwargs) for name, member in self.members.items()}
@@ -1159,7 +1376,7 @@ def modules_collection(self) -> ModulesCollection:
@property
def members(self) -> dict[str, Object | Alias]:
- """The target's members (modules, classes, functions, attributes).
+ """The target's members (modules, classes, functions, attributes, type aliases).
See also: [`inherited_members`][griffe.Alias.inherited_members],
[`get_member`][griffe.Alias.get_member],
@@ -1264,6 +1481,11 @@ def docstring(self) -> Docstring | None:
def docstring(self, docstring: Docstring | None) -> None:
self.final_target.docstring = docstring
+ @property
+ def type_parameters(self) -> TypeParameters:
+ """The target type parameters."""
+ return self.final_target.type_parameters
+
@property
def labels(self) -> set[str]:
"""The target labels (`property`, `dataclass`, etc.).
@@ -1314,6 +1536,7 @@ def is_kind(self, kind: str | Kind | set[str | Kind]) -> bool:
[`is_class`][griffe.Alias.is_class],
[`is_function`][griffe.Alias.is_function],
[`is_attribute`][griffe.Alias.is_attribute],
+ [`is_type_alias`][griffe.Alias.is_type_alias],
[`is_alias`][griffe.Alias.is_alias].
Parameters:
@@ -1335,6 +1558,7 @@ def is_module(self) -> bool:
[`is_class`][griffe.Alias.is_class],
[`is_function`][griffe.Alias.is_function],
[`is_attribute`][griffe.Alias.is_attribute],
+ [`is_type_alias`][griffe.Alias.is_type_alias],
[`is_alias`][griffe.Alias.is_alias],
[`is_kind`][griffe.Alias.is_kind].
"""
@@ -1347,6 +1571,7 @@ def is_class(self) -> bool:
See also: [`is_module`][griffe.Alias.is_module],
[`is_function`][griffe.Alias.is_function],
[`is_attribute`][griffe.Alias.is_attribute],
+ [`is_type_alias`][griffe.Alias.is_type_alias],
[`is_alias`][griffe.Alias.is_alias],
[`is_kind`][griffe.Alias.is_kind].
"""
@@ -1359,6 +1584,7 @@ def is_function(self) -> bool:
See also: [`is_module`][griffe.Alias.is_module],
[`is_class`][griffe.Alias.is_class],
[`is_attribute`][griffe.Alias.is_attribute],
+ [`is_type_alias`][griffe.Alias.is_type_alias],
[`is_alias`][griffe.Alias.is_alias],
[`is_kind`][griffe.Alias.is_kind].
"""
@@ -1371,11 +1597,25 @@ def is_attribute(self) -> bool:
See also: [`is_module`][griffe.Alias.is_module],
[`is_class`][griffe.Alias.is_class],
[`is_function`][griffe.Alias.is_function],
+ [`is_type_alias`][griffe.Alias.is_type_alias],
[`is_alias`][griffe.Alias.is_alias],
[`is_kind`][griffe.Alias.is_kind].
"""
return self.final_target.is_attribute
+ @property
+ def is_type_alias(self) -> bool:
+ """Whether this object is a type alias.
+
+ See also: [`is_module`][griffe.Alias.is_module],
+ [`is_class`][griffe.Alias.is_class],
+ [`is_function`][griffe.Alias.is_function],
+ [`is_attribute`][griffe.Alias.is_attribute],
+ [`is_alias`][griffe.Alias.is_alias],
+ [`is_kind`][griffe.Alias.is_kind].
+ """
+ return self.final_target.is_type_alias
+
def has_labels(self, *labels: str) -> bool:
"""Tell if this object has all the given labels.
@@ -1512,7 +1752,7 @@ def resolve(self, name: str) -> str:
"""
return self.final_target.resolve(name)
- # SPECIFIC MODULE/CLASS/FUNCTION/ATTRIBUTE PROXIES ---------------
+ # SPECIFIC MODULE/CLASS/FUNCTION/ATTRIBUTE/TYPE ALIAS PROXIES ---------------
# These methods and properties exist on targets of specific kind.
# We first try to reach the final target, triggering alias resolution errors
# and cyclic aliases errors early. We avoid recursing in the alias chain.
@@ -1625,8 +1865,8 @@ def deleter(self) -> Function | None:
@property
def value(self) -> str | Expr | None:
- """The attribute value."""
- return cast("Attribute", self.final_target).value
+ """The attribute or type alias value."""
+ return cast("Union[Attribute, TypeAlias]", self.final_target).value
@value.setter
def value(self, value: str | Expr | None) -> None:
@@ -2260,7 +2500,7 @@ def __init__(
"""The deleter linked to this property."""
def as_dict(self, **kwargs: Any) -> dict[str, Any]:
- """Return this function's data as a dictionary.
+ """Return this attribute's data as a dictionary.
See also: [`as_json`][griffe.Attribute.as_json].
@@ -2276,3 +2516,39 @@ def as_dict(self, **kwargs: Any) -> dict[str, Any]:
if self.annotation is not None:
base["annotation"] = self.annotation
return base
+
+
+class TypeAlias(Object):
+ """The class representing a Python type alias."""
+
+ kind = Kind.TYPE_ALIAS
+
+ def __init__(
+ self,
+ *args: Any,
+ value: str | Expr | None = None,
+ **kwargs: Any,
+ ) -> None:
+ """Initialize the function.
+
+ Parameters:
+ *args: See [`griffe.Object`][].
+ value: The type alias value.
+ **kwargs: See [`griffe.Object`][].
+ """
+ super().__init__(*args, **kwargs, runtime=False)
+ self.value: str | Expr | None = value
+ """The type alias value."""
+
+ def as_dict(self, **kwargs: Any) -> dict[str, Any]:
+ """Return this type alias's data as a dictionary.
+
+ Parameters:
+ **kwargs: Additional serialization options.
+
+ Returns:
+ A dictionary.
+ """
+ base = super().as_dict(**kwargs)
+ base["value"] = self.value
+ return base
diff --git a/src/_griffe/stats.py b/src/_griffe/stats.py
index 7b89c0c2..2f1df30f 100644
--- a/src/_griffe/stats.py
+++ b/src/_griffe/stats.py
@@ -46,6 +46,7 @@ def __init__(self, loader: GriffeLoader) -> None:
Kind.CLASS: 0,
Kind.FUNCTION: 0,
Kind.ATTRIBUTE: 0,
+ Kind.TYPE_ALIAS: 0,
}
"""Number of objects by kind."""
@@ -94,7 +95,8 @@ def as_text(self) -> str:
classes = self.by_kind[Kind.CLASS]
functions = self.by_kind[Kind.FUNCTION]
attributes = self.by_kind[Kind.ATTRIBUTE]
- objects = sum((modules, classes, functions, attributes))
+ type_aliases = self.by_kind[Kind.TYPE_ALIAS]
+ objects = sum((modules, classes, functions, attributes, type_aliases))
lines.append("Statistics")
lines.append("---------------------")
lines.append("Number of loaded objects")
@@ -102,6 +104,7 @@ def as_text(self) -> str:
lines.append(f" Classes: {classes}")
lines.append(f" Functions: {functions}")
lines.append(f" Attributes: {attributes}")
+ lines.append(f" Type aliases: {type_aliases}")
lines.append(f" Total: {objects} across {packages} packages")
per_ext = self.modules_by_extension
builtin = per_ext[""]
diff --git a/src/griffe/__init__.py b/src/griffe/__init__.py
index 73667a0c..ea5f9308 100644
--- a/src/griffe/__init__.py
+++ b/src/griffe/__init__.py
@@ -231,8 +231,12 @@
DocstringSectionReceives,
DocstringSectionReturns,
DocstringSectionText,
+ DocstringSectionTypeAliases,
+ DocstringSectionTypeParameters,
DocstringSectionWarns,
DocstringSectionYields,
+ DocstringTypeAlias,
+ DocstringTypeParameter,
DocstringWarn,
DocstringYield,
)
@@ -257,6 +261,7 @@
ObjectKind,
ParameterKind,
Parser,
+ TypeParameterKind,
)
from _griffe.exceptions import (
AliasResolutionError,
@@ -347,6 +352,9 @@
Object,
Parameter,
Parameters,
+ TypeAlias,
+ TypeParameter,
+ TypeParameters,
)
from _griffe.stats import Stats
from _griffe.tests import (
@@ -411,9 +419,13 @@
"DocstringSectionReceives",
"DocstringSectionReturns",
"DocstringSectionText",
+ "DocstringSectionTypeAliases",
+ "DocstringSectionTypeParameters",
"DocstringSectionWarns",
"DocstringSectionYields",
"DocstringStyle",
+ "DocstringTypeAlias",
+ "DocstringTypeParameter",
"DocstringWarn",
"DocstringYield",
"ExplanationStyle",
@@ -500,6 +512,10 @@
"SetMembersMixin",
"Stats",
"TmpPackage",
+ "TypeAlias",
+ "TypeParameter",
+ "TypeParameterKind",
+ "TypeParameters",
"UnhandledEditableModuleError",
"UnimportableModuleError",
"Visitor",
diff --git a/tests/test_docstrings/test_google.py b/tests/test_docstrings/test_google.py
index a7eac41f..8748bad9 100644
--- a/tests/test_docstrings/test_google.py
+++ b/tests/test_docstrings/test_google.py
@@ -7,6 +7,8 @@
import pytest
+from _griffe.enumerations import TypeParameterKind
+from _griffe.models import TypeParameter, TypeParameters
from griffe import (
Attribute,
Class,
@@ -195,10 +197,12 @@ def test_empty_indented_lines_in_section_with_items(parse_google: ParserType) ->
"Attributes",
"Other Parameters",
"Parameters",
+ "Type Parameters",
"Raises",
"Receives",
"Returns",
"Warns",
+ "Type aliases",
"Yields",
],
)
@@ -392,6 +396,30 @@ def test_parse_classes_section(parse_google: ParserType) -> None:
assert not warnings
+def test_parse_type_aliases_section(parse_google: ParserType) -> None:
+ """Parse Type Aliases sections.
+
+ Parameters:
+ parse_google: Fixture parser.
+ """
+ docstring = """
+ Type Aliases:
+ TC: Hello.
+ TD: Hi.
+ """
+
+ sections, warnings = parse_google(docstring)
+ assert len(sections) == 1
+ assert sections[0].kind is DocstringSectionKind.type_aliases
+ type_alias_c = sections[0].value[0]
+ assert type_alias_c.name == "TC"
+ assert type_alias_c.description == "Hello."
+ type_alias_d = sections[0].value[1]
+ assert type_alias_d.name == "TD"
+ assert type_alias_d.description == "Hi."
+ assert not warnings
+
+
def test_parse_modules_section(parse_google: ParserType) -> None:
"""Parse Modules sections.
@@ -899,6 +927,132 @@ def test_class_uses_init_parameters(parse_google: ParserType) -> None:
# assert not warnings
+# =============================================================================================
+# Type parameters sections
+def test_parse_type_var_tuples_and_param_specs(parse_google: ParserType) -> None:
+ """Parse type variable tuples and parameter specifications.
+
+ Parameters:
+ parse_google: Fixture parser.
+ """
+ docstring = """
+ Type Parameters:
+ T: A type parameter.
+ C (str, (int, float)): A constrained type parameter.
+ D complex: A bounded type parameter.
+ """
+
+ sections, warnings = parse_google(docstring)
+ assert len(sections) == 1
+ expected_type_parameters = {
+ "T": ("A type parameter.", None),
+ "C": ("A constrained type parameter.", "str, (int, float)"),
+ "D": ("A bounded type parameter.", "complex"),
+ }
+ for type_parameter in sections[0].value:
+ assert type_parameter.name in expected_type_parameters
+ assert expected_type_parameters[type_parameter.name][0] == type_parameter.description
+ assert expected_type_parameters[type_parameter.name][1] == type_parameter.annotation
+ assert not warnings
+
+
+def test_prefer_docstring_bounds_over_annotations(parse_google: ParserType) -> None:
+ """Prefer the docstring bound over the annotation.
+
+ Parameters:
+ parse_google: Fixture parser.
+ """
+ docstring = """
+ Type Parameters:
+ X (str): X type.
+ Y str, int: Y type.
+ """
+
+ sections, warnings = parse_google(
+ docstring,
+ parent=Function(
+ "func",
+ type_parameters=TypeParameters(
+ TypeParameter("X", kind=TypeParameterKind.type_var, constraints=["complex"]),
+ TypeParameter("Y", kind=TypeParameterKind.type_var, bound="int"),
+ ),
+ ),
+ )
+ assert len(sections) == 1
+ assert not warnings
+
+ assert sections[0].kind is DocstringSectionKind.type_parameters
+
+ (x, y) = sections[0].value
+
+ assert x.name == "X"
+ assert str(x.bound) == "str"
+ assert x.constraints is None
+
+ assert y.name == "Y"
+ assert y.bound is None
+ assert [str(constraint) for constraint in y.constraints] == ["str", "int"]
+
+
+def test_type_parameter_line_without_colon(parse_google: ParserType) -> None:
+ """Warn when missing colon.
+
+ Parameters:
+ parse_google: Fixture parser.
+ """
+ docstring = """
+ Type Parameters:
+ X is an integer type.
+ """
+
+ sections, warnings = parse_google(docstring)
+ assert len(sections) == 0 # empty sections are discarded
+ assert len(warnings) == 1
+ assert "pair" in warnings[0]
+
+
+def test_warn_about_unknown_type_parameters(parse_google: ParserType) -> None:
+ """Warn about unknown type parameters in "Type Parameters" sections.
+
+ Parameters:
+ parse_google: Fixture parser.
+ """
+ docstring = """
+ Type Parameters:
+ X (int): Integer.
+ Y (int): Integer.
+ """
+
+ _, warnings = parse_google(
+ docstring,
+ parent=Function(
+ "func",
+ type_parameters=TypeParameters(
+ TypeParameter("A", kind=TypeParameterKind.type_var),
+ TypeParameter("Y", kind=TypeParameterKind.type_var),
+ ),
+ ),
+ )
+ assert len(warnings) == 1
+ assert "'X' does not appear in the function signature" in warnings[0]
+
+
+def test_unknown_type_params_scan_doesnt_crash_without_type_parameters(parse_google: ParserType) -> None:
+ """Assert we don't crash when parsing type parameters sections and parent object does not have type parameters.
+
+ Parameters:
+ parse_google: Fixture parser.
+ """
+ docstring = """
+ TypeParameters:
+ This (str): This.
+ That: That.
+ """
+
+ _, warnings = parse_google(docstring, parent=Module("mod"))
+ assert not warnings
+
+
# =============================================================================================
# Attributes sections
def test_retrieve_attributes_annotation_from_parent(parse_google: ParserType) -> None:
diff --git a/tests/test_encoders.py b/tests/test_encoders.py
index 7bd80e07..832e2e1b 100644
--- a/tests/test_encoders.py
+++ b/tests/test_encoders.py
@@ -3,11 +3,12 @@
from __future__ import annotations
import json
+import sys
import pytest
from jsonschema import ValidationError, validate
-from griffe import Function, GriffeLoader, Module, Object
+from griffe import Function, GriffeLoader, Module, Object, temporary_visited_module
def test_minimal_data_is_enough() -> None:
@@ -33,6 +34,63 @@ def test_minimal_data_is_enough() -> None:
Function.from_json(minimal)
+# YORE: EOL 3.12: Remove block.
+# YORE: EOL 3.11: Remove line.
+@pytest.mark.skipif(sys.version_info < (3, 12), reason="Python less than 3.12 does not have PEP 695 generics")
+def test_encoding_pep695_generics_without_defaults() -> None:
+ """Test serialization and de-serialization of PEP 695 generics without defaults.
+
+ Defaults are only possible from Python 3.13 onwards.
+ """
+ with temporary_visited_module(
+ """
+ class Class[X: Exception]: pass
+ def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass
+ type TA[T: (int, str)] = dict[str, T]
+ """,
+ ) as module:
+ minimal = module.as_json(full=False)
+ full = module.as_json(full=True)
+ reloaded = Module.from_json(minimal)
+ assert reloaded.as_json(full=False) == minimal
+ assert reloaded.as_json(full=True) == full
+
+ # Also works (but will result in a different type hint).
+ assert Object.from_json(minimal)
+
+ # Won't work if the JSON doesn't represent the type requested.
+ with pytest.raises(TypeError, match="provided JSON object is not of type"):
+ Function.from_json(minimal)
+
+
+# YORE: EOL 3.12: Remove line.
+@pytest.mark.skipif(sys.version_info < (3, 13), reason="Python less than 3.13 does not have defaults in PEP 695 generics") # fmt: skip
+def test_encoding_pep695_generics() -> None:
+ """Test serialization and de-serialization of PEP 695 generics with defaults.
+
+ Defaults are only possible from Python 3.13 onwards.
+ """
+ with temporary_visited_module(
+ """
+ class Class[X: Exception = OSError]: pass
+ def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass
+ type TA[T: (int, str) = str] = dict[str, T]
+ """,
+ ) as module:
+ minimal = module.as_json(full=False)
+ full = module.as_json(full=True)
+ reloaded = Module.from_json(minimal)
+ assert reloaded.as_json(full=False) == minimal
+ assert reloaded.as_json(full=True) == full
+
+ # Also works (but will result in a different type hint).
+ assert Object.from_json(minimal)
+
+ # Won't work if the JSON doesn't represent the type requested.
+ with pytest.raises(TypeError, match="provided JSON object is not of type"):
+ Function.from_json(minimal)
+
+
# use this function in test_json_schema to ease schema debugging
def _validate(obj: dict, schema: dict) -> None:
if "members" in obj:
@@ -55,3 +113,44 @@ def test_json_schema() -> None:
with open("docs/schema.json") as f: # noqa: PTH123
schema = json.load(f)
validate(data, schema)
+
+
+# YORE: EOL 3.12: Remove block.
+# YORE: EOL 3.11: Remove line.
+@pytest.mark.skipif(sys.version_info < (3, 12), reason="Python less than 3.12 does not have PEP 695 generics")
+def test_json_schema_for_pep695_generics_without_defaults() -> None:
+ """Assert that serialized PEP 695 generics without defaults match our JSON schema.
+
+ Defaults are only possible from Python 3.13 onwards.
+ """
+ with temporary_visited_module(
+ """
+ class Class[X: Exception]: pass
+ def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass
+ type TA[T: (int, str)] = dict[str, T]
+ """,
+ ) as module:
+ data = json.loads(module.as_json(full=True))
+ with open("docs/schema.json") as f: # noqa: PTH123
+ schema = json.load(f)
+ validate(data, schema)
+
+
+# YORE: EOL 3.12: Remove line.
+@pytest.mark.skipif(sys.version_info < (3, 13), reason="Python less than 3.13 does not have defaults in PEP 695 generics") # fmt: skip
+def test_json_schema_for_pep695_generics() -> None:
+ """Assert that serialized PEP 695 generics with defaults match our JSON schema.
+
+ Defaults are only possible from Python 3.13 onwards.
+ """
+ with temporary_visited_module(
+ """
+ class Class[X: Exception = OSError]: pass
+ def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass
+ type TA[T: (int, str) = str] = dict[str, T]
+ """,
+ ) as module:
+ data = json.loads(module.as_json(full=True))
+ with open("docs/schema.json") as f: # noqa: PTH123
+ schema = json.load(f)
+ validate(data, schema)
diff --git a/tests/test_expressions.py b/tests/test_expressions.py
index f2d326a7..e9030e6d 100644
--- a/tests/test_expressions.py
+++ b/tests/test_expressions.py
@@ -3,6 +3,7 @@
from __future__ import annotations
import ast
+import sys
import pytest
@@ -140,3 +141,25 @@ def test_parentheses_preserved(code: str) -> None:
with temporary_visited_module(f"val = {code}") as module:
value_expr = module["val"].value
assert str(value_expr) == code
+
+
+# YORE: EOL 3.11: Remove line.
+@pytest.mark.skipif(sys.version_info < (3, 12), reason="Python less than 3.12 does not have PEP 695 generics")
+def test_resolving_type_parameters() -> None:
+ """Assert type parameters are correctly transformed to their fully-resolved form."""
+ with temporary_visited_module(
+ """
+ class C[T]:
+ class D[T]:
+ def func[Y](self, arg1: T, arg2: Y): pass
+ attr: T
+ def func[Z](arg1: T, arg2: Y): pass
+ """,
+ ) as module:
+ assert module["C.D.func"].parameters["arg1"].annotation.canonical_path == "module.C.D[T]"
+ assert module["C.D.func"].parameters["arg2"].annotation.canonical_path == "module.C.D.func[Y]"
+
+ assert module["C.D.attr"].annotation.canonical_path == "module.C.D[T]"
+
+ assert module["C.func"].parameters["arg1"].annotation.canonical_path == "module.C[T]"
+ assert module["C.func"].parameters["arg2"].annotation.canonical_path == "Y"
diff --git a/tests/test_extensions.py b/tests/test_extensions.py
index 773a03f1..f1a5c385 100644
--- a/tests/test_extensions.py
+++ b/tests/test_extensions.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any
@@ -12,7 +13,7 @@
if TYPE_CHECKING:
import ast
- from griffe import Attribute, Class, Function, Module, Object, ObjectNode
+ from griffe import Attribute, Class, Function, Module, Object, ObjectNode, TypeAlias
class ExtensionTest(Extension): # noqa: D101
@@ -61,6 +62,12 @@ def on_module_node(self, *, node: ast.AST | ObjectNode, **kwargs: Any) -> None:
def on_node(self, *, node: ast.AST | ObjectNode, **kwargs: Any) -> None: # noqa: D102,ARG002
self.records.append("on_node")
+ def on_type_alias_instance(self, *, node: ast.AST | ObjectNode, type_alias: TypeAlias, **kwargs: Any) -> None: # noqa: D102,ARG002
+ self.records.append("on_type_alias_instance")
+
+ def on_type_alias_node(self, *, node: ast.AST | ObjectNode, **kwargs: Any) -> None: # noqa: D102,ARG002
+ self.records.append("on_type_alias_node")
+
@pytest.mark.parametrize(
"extension",
@@ -102,6 +109,41 @@ def test_loading_extensions(extension: str | dict[str, dict[str, Any]] | Extensi
assert loaded.kwargs == {"option": 0}
+# YORE: EOL 3.11: Remove block.
+def test_extension_events_without_type_aliases() -> None:
+ """Test events triggering."""
+ extension = ExtensionTest()
+ with temporary_visited_module(
+ """
+ attr = 0
+ def func(): ...
+ class Class:
+ cattr = 1
+ def method(self): ...
+ """,
+ extensions=load_extensions(extension),
+ ):
+ pass
+ events = [
+ "on_attribute_instance",
+ "on_attribute_node",
+ "on_class_instance",
+ "on_class_members",
+ "on_class_node",
+ "on_function_instance",
+ "on_function_node",
+ "on_instance",
+ "on_members",
+ "on_module_instance",
+ "on_module_members",
+ "on_module_node",
+ "on_node",
+ ]
+ assert set(events) == set(extension.records)
+
+
+# YORE: EOL 3.11: Remove line.
+@pytest.mark.skipif(sys.version_info < (3, 12), reason="Python less than 3.12 does not have PEP 695 type aliases")
def test_extension_events() -> None:
"""Test events triggering."""
extension = ExtensionTest()
@@ -112,6 +154,7 @@ def func(): ...
class Class:
cattr = 1
def method(self): ...
+ type TypeAlias = list[int]
""",
extensions=load_extensions(extension),
):
@@ -130,5 +173,7 @@ def method(self): ...
"on_module_members",
"on_module_node",
"on_node",
+ "on_type_alias_instance",
+ "on_type_alias_node",
]
assert set(events) == set(extension.records)
diff --git a/tests/test_inspector.py b/tests/test_inspector.py
index 0a3da529..9dd5d242 100644
--- a/tests/test_inspector.py
+++ b/tests/test_inspector.py
@@ -6,6 +6,8 @@
import pytest
+from _griffe.enumerations import TypeParameterKind
+from _griffe.expressions import Expr
from griffe import inspect, temporary_inspected_module, temporary_inspected_package, temporary_pypackage
from tests.helpers import clear_sys_modules
@@ -238,3 +240,102 @@ class B(A): ...
assert module["B.B.B"].final_target is module["A.B"]
assert module["A.B.B.B"].final_target is module["A.B"]
assert module["B.B.B.B"].final_target is module["A.B"]
+
+
+# YORE: EOL 3.12: Remove block.
+# YORE: EOL 3.11: Remove line.
+@pytest.mark.skipif(sys.version_info < (3, 12), reason="Python less than 3.12 does not have PEP 695 generics")
+def test_inspecting_pep695_generics_without_defaults() -> None:
+ """Assert PEP 695 generics are correctly inspected."""
+ with temporary_inspected_module(
+ """
+ class Class[X: Exception]: pass
+ def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass
+ type TA[T: (int, str)] = dict[str, T]
+ """,
+ ) as module:
+ class_ = module["Class"]
+ assert class_.is_class
+ assert class_.type_parameters[0].name == "X"
+ assert class_.type_parameters[0].kind == TypeParameterKind.type_var
+ assert class_.type_parameters[0].bound.name == "Exception"
+ assert not class_.type_parameters[0].constraints
+ assert class_.type_parameters[0].default is None
+
+ func = module["func"]
+ assert func.is_function
+ assert func.type_parameters[0].name == "P"
+ assert func.type_parameters[0].kind == TypeParameterKind.param_spec
+ assert func.type_parameters[0].bound is None
+ assert not func.type_parameters[0].constraints
+ assert func.type_parameters[0].default is None
+ assert func.type_parameters[1].name == "T"
+ assert func.type_parameters[1].kind == TypeParameterKind.type_var
+ assert func.type_parameters[1].bound is None
+ assert not func.type_parameters[1].constraints
+ assert func.type_parameters[1].default is None
+ assert func.type_parameters[2].name == "R"
+ assert func.type_parameters[2].kind == TypeParameterKind.type_var_tuple
+ assert func.type_parameters[2].bound is None
+ assert not func.type_parameters[2].constraints
+ assert func.type_parameters[2].default is None
+
+ type_alias = module["TA"]
+ assert type_alias.is_type_alias
+ assert type_alias.type_parameters[0].name == "T"
+ assert type_alias.type_parameters[0].kind == TypeParameterKind.type_var
+ assert type_alias.type_parameters[0].bound is None
+ assert type_alias.type_parameters[0].constraints[0].name == "int"
+ assert type_alias.type_parameters[0].constraints[1].name == "str"
+ assert type_alias.type_parameters[0].default is None
+ assert isinstance(type_alias.value, Expr)
+ assert str(type_alias.value) == "dict[str, T]"
+
+
+# YORE: EOL 3.12: Remove line.
+@pytest.mark.skipif(sys.version_info < (3, 13), reason="Python less than 3.13 does not have defaults in PEP 695 generics") # fmt: skip
+def test_inspecting_pep695_generics() -> None:
+ """Assert PEP 695 generics are correctly inspected."""
+ with temporary_inspected_module(
+ """
+ class Class[X: Exception = OSError]: pass
+ def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass
+ type TA[T: (int, str) = str] = dict[str, T]
+ """,
+ ) as module:
+ class_ = module["Class"]
+ assert class_.is_class
+ assert class_.type_parameters[0].name == "X"
+ assert class_.type_parameters[0].kind == TypeParameterKind.type_var
+ assert class_.type_parameters[0].bound.name == "Exception"
+ assert not class_.type_parameters[0].constraints
+ assert class_.type_parameters[0].default.name == "OSError"
+
+ func = module["func"]
+ assert func.is_function
+ assert func.type_parameters[0].name == "P"
+ assert func.type_parameters[0].kind == TypeParameterKind.param_spec
+ assert func.type_parameters[0].bound is None
+ assert not func.type_parameters[0].constraints
+ assert func.type_parameters[0].default is None
+ assert func.type_parameters[1].name == "T"
+ assert func.type_parameters[1].kind == TypeParameterKind.type_var
+ assert func.type_parameters[1].bound is None
+ assert not func.type_parameters[1].constraints
+ assert func.type_parameters[1].default is None
+ assert func.type_parameters[2].name == "R"
+ assert func.type_parameters[2].kind == TypeParameterKind.type_var_tuple
+ assert func.type_parameters[2].bound is None
+ assert not func.type_parameters[2].constraints
+ assert func.type_parameters[2].default is None
+
+ type_alias = module["TA"]
+ assert type_alias.is_type_alias
+ assert type_alias.type_parameters[0].name == "T"
+ assert type_alias.type_parameters[0].kind == TypeParameterKind.type_var
+ assert type_alias.type_parameters[0].bound is None
+ assert type_alias.type_parameters[0].constraints[0].name == "int"
+ assert type_alias.type_parameters[0].constraints[1].name == "str"
+ assert type_alias.type_parameters[0].default.name == "str"
+ assert isinstance(type_alias.value, Expr)
+ assert str(type_alias.value) == "dict[str, T]"
diff --git a/tests/test_models.py b/tests/test_models.py
index 25d4c02a..e9721555 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -2,11 +2,14 @@
from __future__ import annotations
+import sys
from copy import deepcopy
from textwrap import dedent
import pytest
+from _griffe.enumerations import TypeParameterKind
+from _griffe.models import TypeParameter, TypeParameters
from griffe import (
Attribute,
Class,
@@ -593,3 +596,62 @@ def test_building_function_and_class_signatures() -> None:
func = Function("test_function", parameters=params, returns="None")
expected = "test_function(a, b: int = 0, /, c, d: str = '', *args, e, f: bool = False, **kwargs) -> None"
assert func.signature() == expected
+
+
+def test_set_type_parameters() -> None:
+ """We can set type parameters."""
+ type_parameters = TypeParameters()
+ # Does not exist yet.
+ type_parameters["x"] = TypeParameter(name="x", kind=TypeParameterKind.type_var)
+ assert "x" in type_parameters
+ # Already exists, by name.
+ type_parameters["x"] = TypeParameter(name="x", kind=TypeParameterKind.type_var)
+ assert "x" in type_parameters
+ assert len(type_parameters) == 1
+ # Already exists, by name, with different kind.
+ type_parameters["x"] = TypeParameter(name="x", kind=TypeParameterKind.param_spec)
+ assert "x" in type_parameters
+ assert len(type_parameters) == 1
+ # Already exists, by index.
+ type_parameters[0] = TypeParameter(name="y", kind=TypeParameterKind.type_var)
+ assert "y" in type_parameters
+ assert len(type_parameters) == 1
+
+
+def test_delete_type_parameters() -> None:
+ """We can delete type parameters."""
+ type_parameters = TypeParameters()
+ # By name.
+ type_parameters["x"] = TypeParameter(name="x", kind=TypeParameterKind.type_var)
+ del type_parameters["x"]
+ assert "x" not in type_parameters
+ assert len(type_parameters) == 0
+ # By index.
+ type_parameters["x"] = TypeParameter(name="x", kind=TypeParameterKind.type_var)
+ del type_parameters[0]
+ assert "x" not in type_parameters
+ assert len(type_parameters) == 0
+
+
+# YORE: EOL 3.11: Remove line.
+@pytest.mark.skipif(sys.version_info < (3, 12), reason="Python less than 3.12 does not have PEP 695 generics")
+def test_annotation_resolution() -> None:
+ """Names are correctly resolved in the annotation scope of an object."""
+ with temporary_visited_module(
+ """
+ class C[T]:
+ class D[T]:
+ def func[Y](self, arg1: T, arg2: Y): pass
+ def func[Z](arg1: T, arg2: Y): pass
+ """,
+ ) as module:
+ assert module["C.D"].resolve("T") == "module.C.D[T]"
+
+ assert module["C.D.func"].resolve("T") == "module.C.D[T]"
+ assert module["C.D.func"].resolve("Y") == "module.C.D.func[Y]"
+
+ assert module["C"].resolve("T") == "module.C[T]"
+
+ assert module["C.func"].resolve("T") == "module.C[T]"
+ with pytest.raises(NameResolutionError):
+ module["C.func"].resolve("Y")
diff --git a/tests/test_visitor.py b/tests/test_visitor.py
index a2bfa4ba..c7dca0e6 100644
--- a/tests/test_visitor.py
+++ b/tests/test_visitor.py
@@ -2,10 +2,13 @@
from __future__ import annotations
+import sys
from textwrap import dedent
import pytest
+from _griffe.enumerations import TypeParameterKind
+from _griffe.expressions import Expr
from griffe import GriffeLoader, temporary_pypackage, temporary_visited_module, temporary_visited_package
@@ -389,3 +392,102 @@ def test_parse_deep_attributes_in__all__() -> None:
},
) as package:
assert "hello" in package.exports # type: ignore[operator]
+
+
+# YORE: EOL 3.12: Remove block.
+# YORE: EOL 3.11: Remove line.
+@pytest.mark.skipif(sys.version_info < (3, 12), reason="Python less than 3.12 does not have PEP 695 generics")
+def test_parse_pep695_generics_without_defaults() -> None:
+ """Assert PEP 695 generics are correctly inspected."""
+ with temporary_visited_module(
+ """
+ class Class[X: Exception]: pass
+ def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass
+ type TA[T: (int, str)] = dict[str, T]
+ """,
+ ) as module:
+ class_ = module["Class"]
+ assert class_.is_class
+ assert class_.type_parameters[0].name == "X"
+ assert class_.type_parameters[0].kind == TypeParameterKind.type_var
+ assert class_.type_parameters[0].bound.name == "Exception"
+ assert not class_.type_parameters[0].constraints
+ assert class_.type_parameters[0].default is None
+
+ func = module["func"]
+ assert func.is_function
+ assert func.type_parameters[0].name == "P"
+ assert func.type_parameters[0].kind == TypeParameterKind.param_spec
+ assert func.type_parameters[0].bound is None
+ assert not func.type_parameters[0].constraints
+ assert func.type_parameters[0].default is None
+ assert func.type_parameters[1].name == "T"
+ assert func.type_parameters[1].kind == TypeParameterKind.type_var
+ assert func.type_parameters[1].bound is None
+ assert not func.type_parameters[1].constraints
+ assert func.type_parameters[1].default is None
+ assert func.type_parameters[2].name == "R"
+ assert func.type_parameters[2].kind == TypeParameterKind.type_var_tuple
+ assert func.type_parameters[2].bound is None
+ assert not func.type_parameters[2].constraints
+ assert func.type_parameters[2].default is None
+
+ type_alias = module["TA"]
+ assert type_alias.is_type_alias
+ assert type_alias.type_parameters[0].name == "T"
+ assert type_alias.type_parameters[0].kind == TypeParameterKind.type_var
+ assert type_alias.type_parameters[0].bound is None
+ assert type_alias.type_parameters[0].constraints[0].name == "int"
+ assert type_alias.type_parameters[0].constraints[1].name == "str"
+ assert type_alias.type_parameters[0].default is None
+ assert isinstance(type_alias.value, Expr)
+ assert str(type_alias.value) == "dict[str, T]"
+
+
+# YORE: EOL 3.12: Remove line.
+@pytest.mark.skipif(sys.version_info < (3, 13), reason="Python less than 3.13 does not have defaults in PEP 695 generics") # fmt: skip
+def test_parse_pep695_generics() -> None:
+ """Assert PEP 695 generics are correctly parsed."""
+ with temporary_visited_module(
+ """
+ class Class[X: Exception = OSError]: pass
+ def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass
+ type TA[T: (int, str) = str] = dict[str, T]
+ """,
+ ) as module:
+ class_ = module["Class"]
+ assert class_.is_class
+ assert class_.type_parameters[0].name == "X"
+ assert class_.type_parameters[0].kind == TypeParameterKind.type_var
+ assert class_.type_parameters[0].bound.name == "Exception"
+ assert not class_.type_parameters[0].constraints
+ assert class_.type_parameters[0].default.name == "OSError"
+
+ func = module["func"]
+ assert func.is_function
+ assert func.type_parameters[0].name == "P"
+ assert func.type_parameters[0].kind == TypeParameterKind.param_spec
+ assert func.type_parameters[0].bound is None
+ assert not func.type_parameters[0].constraints
+ assert func.type_parameters[0].default is None
+ assert func.type_parameters[1].name == "T"
+ assert func.type_parameters[1].kind == TypeParameterKind.type_var
+ assert func.type_parameters[1].bound is None
+ assert not func.type_parameters[1].constraints
+ assert func.type_parameters[1].default is None
+ assert func.type_parameters[2].name == "R"
+ assert func.type_parameters[2].kind == TypeParameterKind.type_var_tuple
+ assert func.type_parameters[2].bound is None
+ assert not func.type_parameters[2].constraints
+ assert func.type_parameters[2].default is None
+
+ type_alias = module["TA"]
+ assert type_alias.is_type_alias
+ assert type_alias.type_parameters[0].name == "T"
+ assert type_alias.type_parameters[0].kind == TypeParameterKind.type_var
+ assert type_alias.type_parameters[0].bound is None
+ assert type_alias.type_parameters[0].constraints[0].name == "int"
+ assert type_alias.type_parameters[0].constraints[1].name == "str"
+ assert type_alias.type_parameters[0].default.name == "str"
+ assert isinstance(type_alias.value, Expr)
+ assert str(type_alias.value) == "dict[str, T]"
|