Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Starting docs schema and translation methods #24

Merged
merged 4 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions autoflex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .install import install_verification
from autoflex.directives import AutoFlex
from autoflex.styles.setup import copy_autoflex_styles_to_static
from autoflex.extractors import determine_pydantic_version_from_base_model, get_field_infos

__version__ = "0.0.1"
__author__ = "Dario Quintero Dominguez"
Expand Down
11 changes: 11 additions & 0 deletions autoflex/constructors/field_info_to_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
This file contains all the functionality to convert from a compiled `AutoflexFieldInfo` or standard pydantic `FieldInfo`
into a AutoflexPropertyType. This way all the fields and annotations can be compiled into a standardized data type
"""
from autoflex.types import FieldTypes

def compile_field_into_property(
field: FieldTypes
):
"""
"""
18 changes: 18 additions & 0 deletions autoflex/constructors/parameter_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
This file contains the structure of a ParameterTable.
"""
from autoflex.types import PropertyTypes

def extract_class_to_parameter_table(physcial_parameter: PropertyTypes) -> ParameterTable:
"""
This method converts from a given class or schema declaration into a container of data required for a ParameterTable
in its intended implementation.
"""

def parameter_table_to_nodes() -> list:
"""
This function converts the data container within a parameter table to sphinx nodes which can be used within both
a given directive as the automated construction of a class based on an internal function.

Should be a list of nodes that convert to sphinx.
"""
File renamed without changes.
5 changes: 5 additions & 0 deletions autoflex/directives/autoflex.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ class AutoFlex(SphinxDirective):
The ``autoflex`` directive improves the data structure generated during the `autosummary` process
instead of the `automodule` or `autoclass` directive.

There are three components to the autoflex class:
- Extract the relevant docs schema
- Convert that docs schema into a documentation structure data type
- Construct those into doctree nodes within sphinx. These are then returned to the directive declaration.

Usage
-----

Expand Down
134 changes: 134 additions & 0 deletions autoflex/extractors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""
Note that the compilation flow is as follows:

.. code::

FieldTypes -> PropertyTypes
PhysicalFieldInfo -> PhysicalProperty
pd.FieldInfo -> Property

Then, this automated flow can be run for a given BaseModel and the properties can be extracted accordingly.
"""
import pydantic as pd
from autoflex.types import PropertyTypes, FieldTypes, PhysicalProperty, PhysicalFieldInfo, Property
from autoflex.version_manager import determine_pydantic_version_from_base_model

def get_field_infos(model: pd.BaseModel):
"""Get all FieldInfo instances from a Pydantic model, compatible with v1 and v2."""
version = determine_pydantic_version_from_base_model(model)

field_infos = []

# Handle Pydantic v2
if version == 2:
for field_name, field in model.model_fields.items():
field_infos.append(field)

# Handle Pydantic v1
elif version == 1:
for field_name, field in model.__fields__.items():
field_infos.append(field)

return field_infos


def physical_field_info_to_physical_property(field: PhysicalFieldInfo, field_name: str) -> PhysicalProperty:
"""
Convert a PhysicalFieldInfo instance to a PhysicalProperty.

Args:
field: The PhysicalFieldInfo instance.
field_name: The name of the field.

Returns:
PhysicalProperty: The corresponding PhysicalProperty instance.
"""
# Extract unit and math information
unit = field.unit
math = field.math

# Extract other field attributes
description = field.description or ""
default = field.default if field.default is not None else ""

# Assuming 'types' can be inferred from the field's type
types = str(field.outer_type_) if hasattr(field, 'outer_type_') else ""

return PhysicalProperty(
name=field_name,
types=types,
description=description,
default=str(default),
unit=unit,
math=math
)


def field_info_to_property(field: pd.fields.FieldInfo, field_name: str) -> Property:
"""
Convert a standard FieldInfo instance to a Property.

Args:
field: The FieldInfo instance.
field_name: The name of the field.

Returns:
Property: The corresponding Property instance.
"""
description = field.description or ""
default = field.default if field.default is not None else ""
types = str(field.outer_type_) if hasattr(field, 'outer_type_') else ""

return Property(
name=field_name,
types=types,
description=description,
default=str(default)
)


def auto_field_to_property_type(field: FieldTypes, field_name: str) -> PropertyTypes:
"""
Convert a FieldTypes instance to a PropertyTypes instance.

Args:
field: The FieldTypes instance (PhysicalFieldInfo or FieldInfo).
field_name: The name of the field.

Returns:
PropertyTypes: The corresponding PropertyTypes instance.
"""
if isinstance(field, PhysicalFieldInfo):
return physical_field_info_to_physical_property(field, field_name)
else:
return field_info_to_property(field, field_name)


def extract_property_list_from_model(model: pd.BaseModel) -> list[PropertyTypes]:
"""
Extract a list of PropertyTypes from a Pydantic model.

Args:
model: The Pydantic BaseModel instance.

Returns:
List[PropertyTypes]: A list of PropertyTypes instances extracted from the model.
"""
field_infos = get_field_infos(model)
properties = []

# Get field names based on Pydantic version
version = determine_pydantic_version_from_base_model(model)
if version == 2:
field_names = list(model.model_fields.keys())
elif version == 1:
field_names = list(model.__fields__.keys())
else:
field_names = []

for field, field_name in zip(field_infos, field_names):
property_item = auto_field_to_property_type(field, field_name)
properties.append(property_item)

return properties

35 changes: 35 additions & 0 deletions autoflex/field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Any, Optional
from pydantic import Field
from autoflex.types import PhysicalFieldInfo, UnitTypes, SymbolicTypes


def PhysicalField(
default: Any = ...,
*,
unit: UnitTypes,
math: SymbolicTypes,
**kwargs
) -> PhysicalFieldInfo:
"""
A wrapper around pydantic's Field function that returns an instance of AutoflexFieldInfo
instead of FieldInfo.

Args:
default: The default value of the field.
unit: The UnitType to represent.
math: The SymbolicType to represent.
**kwargs: Any other keyword arguments passed to pydantic's Field.

Returns:
AutoflexFieldInfo: Custom field info object.
"""

# Need to compile this into a FieldInfo in order to extract the correct kwargs.
field_info = Field(default=default, **kwargs)

# Return an instance of AutoflexFieldInfo instead of the default FieldInfo
return PhysicalFieldInfo(
default=default,
unit=unit,
math=math,
)
4 changes: 4 additions & 0 deletions autoflex/types/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from autoflex.types.property import PhysicalProperty, Property, PropertyTypes
from autoflex.types.descriptors import Symbolic, SymbolicTypes, Unit, UnitTypes
from autoflex.types.field_info import PhysicalFieldInfo, FieldTypes
from autoflex.types.field_info import PhysicalFieldInfo
9 changes: 9 additions & 0 deletions autoflex/types/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pydantic import BaseModel

class AutoflexBaseModel(BaseModel):
"""
A base class that can be used for any model within the system.
It inherits from Pydantic BaseModel to leverage data validation
and parsing features.
"""
pass
36 changes: 36 additions & 0 deletions autoflex/types/descriptors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
This contains all the relevant types used for the documentation constructors and base definition.
"""
import pydantic.fields
from pydantic import Field
from typing import Union
from autoflex.types.core import AutoflexBaseModel

class Symbolic(AutoflexBaseModel):
"""
A class representing a symbolic representation of a label and math formula.

Attributes:
label: The label for the symbolic expression (e.g., 'Force').
math: The mathematical formula or representation (e.g., 'F = ma').
"""
label: str = Field(..., description="Label of the symbolic representation")
math: str = Field(..., description="Mathematical representation or equation")


SymbolicTypes = Union[str, Symbolic]

class Unit(AutoflexBaseModel):
"""
A class representing a physical unit.

Attributes:
name: The name of the unit (e.g., 'meter', 'second').
symbol: The symbol for the unit (e.g., 'm', 's').
description: An optional description of the unit.
"""
name: str = Field(..., description="Name of the unit")
symbol: SymbolicTypes = Field(..., description="Symbol for the unit")
description: str = Field(None, description="Optional description of the unit")

UnitTypes = Union[str, Unit]
20 changes: 20 additions & 0 deletions autoflex/types/field_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import pydantic as pd
from autoflex.types.descriptors import UnitTypes, SymbolicTypes

class PhysicalFieldInfo(pd.fields.FieldInfo):
"""
Each field should correspond to an individual physical property field that can represent it completely within the documentation.

Note that this compiles into a PhysicalProperty accordingly.
"""

unit: UnitTypes = ""
"""
"""

math: SymbolicTypes = ""
"""
"""


FieldTypes = PhysicalFieldInfo | pd.fields.FieldInfo
27 changes: 27 additions & 0 deletions autoflex/types/property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from pydantic import Field
from autoflex.types.core import AutoflexBaseModel
from autoflex.types.descriptors import UnitTypes, SymbolicTypes

class Property(AutoflexBaseModel):
"""
Contains all the information encoded in a standard property.
"""
name: str = ""
types: str = ""
description: str = ""
default: str = ""


class PhysicalProperty(Property):
"""
This structure instance is a representation of the relevant information that might want to represent in a parameter
row or in another container.

We need the parameter name, the type definition in a format we might want to represent (or even interact with)
a description which may be
"""
math: SymbolicTypes = Field(..., description="The mathematical representation defining the physical parameter in raw string latex")
unit: UnitTypes = Field(..., description="The unit of the physical parameter")


PropertyTypes = PhysicalProperty | Property
6 changes: 6 additions & 0 deletions autoflex/types/structures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from autoflex.types import PropertyTypes

PropertyCollectionTable = list[PropertyTypes]
"""
A property table can be compiled from a list of compiled AutoflexProperties in a standard data type format.
"""
5 changes: 5 additions & 0 deletions autoflex/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pydantic.fields import FieldInfo

def check_json_schema_extra(field: FieldInfo) -> bool:
"""Check if the FieldInfo contains a 'json_schema_extra' parameter."""
return hasattr(field, 'json_schema_extra') and field.json_schema_extra is not None
10 changes: 10 additions & 0 deletions autoflex/version_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import pydantic as pd

def determine_pydantic_version_from_base_model(model: pd.BaseModel):
"""Determine if a BaseModel is from Pydantic v1 or v2."""
if hasattr(model, 'model_fields'):
return 2
elif hasattr(model, '__fields__'):
return 1
else:
raise ValueError("Unknown Pydantic version or incompatible BaseModel class.")
2 changes: 1 addition & 1 deletion demo/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .basic_class import BasicClass
from .basic_class import BasicClass, BasicMixedAnnotatedClass
8 changes: 6 additions & 2 deletions demo/basic_class.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pydantic
import pydantic as pd


class BasicClass(pydantic.BaseModel):
class BasicClass(pd.BaseModel):
my_parameter_1: int = 1
my_parameter_2: str = 2
my_parameter_3: float = 3.0
Expand All @@ -25,3 +25,7 @@ def my_static_method():
This is my static method.
"""
pass


class BasicMixedAnnotatedClass(BasicClass):
my_parameter_4: str = pd.Field("" , description="This is my field parameter.", json_schema_extra={})
11 changes: 11 additions & 0 deletions docs/api/descriptors.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Descriptors
------------

Part of the goal of using dedicated ``AutoflexField`` definitions is that we can compile this data into a nice way
to visualise it both in the terminal and on the web. It can be used as a more helpful visualisation tool than the standard
type descriptions.

We want to provide the implementation of the Field functionality so that an `AutoflexField` compiles into a standard
Pydantic Field whilst using the ``json_schema_extra`` to encode the information accordingly.


5 changes: 5 additions & 0 deletions docs/api/fields.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@


.. code::


6 changes: 6 additions & 0 deletions docs/api/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
API
====

.. toctree::

descriptors
Loading
Loading