dan-fritchman / Hdl21

Hardware Description Library
BSD 3-Clause "New" or "Revised" License
60 stars 13 forks source link

Pydantic Errors for Hdl21 Types #174

Open dan-fritchman opened 11 months ago

dan-fritchman commented 11 months ago

There are quite a few places in Hdl21 where we recognize it will be a common error to confuse one type of thing for another, and provide some very targeted user feedback to help with the confusion. Module._attr_type_error serves as a prime example, which does stuff like so:

https://github.com/dan-fritchman/Hdl21/blob/524373b7f4b8f14a47f2ee5287863fe0ff58d630/hdl21/module.py#L375

    if isinstance(val, (Generator, Primitive, ExternalModule)):
        msg = f"Cannot add `{type(val).__name__}` `{val.name}` to `Module` `{m.name}`. Did you mean to make an `Instance` by *calling* it - once for params and once for connections - first?"
    elif isinstance(val, Module):
        msg = f"Cannot add `{type(val).__name__}` `{val.name}` to `Module` `{m.name}`. Did you mean to make an `Instance` by *calling* to connect it first?"
    elif isinstance(val, (GeneratorCall, PrimitiveCall, ExternalModuleCall)):
        msg = f"Cannot add `{type(val).__name__}` `{val}` to `Module` `{m.name}`. Did you mean to make an `Instance` by *calling* to connect it first?"
    else:
        msg = f"Invalid Module attribute {val} of type {type(val)} for {m}. Valid `Module` attributes are of types: {list(ModuleAttr.__args__)}"
    raise TypeError(msg)

One place these errors do not show up - and we have not been providing great feedback - is in the ValidationErrors commonly generated by our many Pydantic dataclasses. The feedback is particularly poor for union types, like from this snippet here:

import hdl21 as h 

@h.paramclass 
class MyParams:
    m = h.Param(dtype=h.Instantiable, desc="Module to instantiate")

MyParams(h.Mos) # <= Look here

Produces:

pydantic.error_wrappers.ValidationError: 4 validation errors for MyParams
m
  instance of Module expected (type=type_error.arbitrary_type; expected_arbitrary_type=Module)
m
  instance of ExternalModuleCall, tuple or dict expected (type=type_error.dataclass; class_name=ExternalModuleCall)
m
  instance of GeneratorCall expected (type=type_error.arbitrary_type; expected_arbitrary_type=GeneratorCall)
m
  instance of PrimitiveCall, tuple or dict expected (type=type_error.dataclass; class_name=PrimitiveCall)

Since it's probably pretty unclear, the fix is:

MyParams(h.Mos()) # <= Note `Mos()` here

Good news is, we learned quite a bit about customizing these Pydantic validations while upgrading our Scalar numeric type. Scalar is now more or less this:

class Scalar(BaseModel):
    """
    Generally this means
    ````python
    Union[Prefixed, Literal]
with built-in automatic conversions from each of:
```python
[str, int, float, Decimal]
```
when used in `paramclass` definitions.
"""
__root__: Union[Prefixed, Literal]


Seems we could apply this to a few more. Or at least one - `Instantiable` - would help quite a lot. 
dan-fritchman commented 4 months ago

Related: #157