diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e357c2d5..8c1cf543 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,28 +16,32 @@ jobs: # Notes on this matrix, particularly the dependencies part: # Hdl21 has two external dependencies: Pydantic and VLSIR. - # + # # 1. VLSIR # This sets up testing with dependencies from both (a) PyPi and (b) "dev" versions from GitHub. # Not every version of Hdl21 is designed to work with both. # Eventually this should know which *should* work. # For now it asserts that the dev-mode passes, and allows failer (`continue-on-error`) for the PyPi version. - # + # # 2. Pydantic # Test with both the minimum supported version, and whatever pip selects, which is generally the latest. # Some languages/ libraries (ahem, Rust) find a way to build in this "test them min supported version" thing; - # we haven't seen one for Python, and can only really do this "manually" because there is only one. + # we haven't seen one for Python, and can only really do this "manually" because there is only one. # Note the combinations pf `python-version` and `pydantic-version` are often relevant; - # typing-stuff generally evolves materially with each python version, and pydantic makes heavy use + # typing-stuff generally evolves materially with each python version, and pydantic makes heavy use # of checking the current interpreter-version to try to make maximally detailed type-checking. - # + # strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] - pydantic-version: ["==1.9.0", ""] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + pydantic-version: ["==1.9.0", "==2.0", "==2.6", ""] dep-installer: ["dev", "pypi"] - continue-on-error: ${{ matrix.dep-installer == 'pypi' || matrix.python-version == '3.11' }} + # Issues for these: + # VLSIR/ PyPi: https://github.com/dan-fritchman/Hdl21/issues/216 + # Python 3.12: https://github.com/dan-fritchman/Hdl21/issues/215 + # Python 3.7-3.8: https://github.com/dan-fritchman/Hdl21/issues/217 + continue-on-error: ${{ matrix.dep-installer == 'pypi' || matrix.python-version == '3.12' || matrix.python-version == '3.7'|| matrix.python-version == '3.8' }} steps: - name: Checkout Repo diff --git a/conftest.py b/conftest.py index 964dbd1c..99b0de51 100644 --- a/conftest.py +++ b/conftest.py @@ -4,9 +4,16 @@ Primarily adds the simulator-focused options enabled by `VlsirTools`. """ +import sys +from pydantic import __version__ as pydantic_version from vlsirtools.pytest import ( pytest_configure, pytest_addoption, pytest_collection_modifyitems, ) + +# Print some config info: the interpreter version, and the pydantic version +print("Hdl21 PyTest Configuration") +print("Python " + sys.version) +print("Pydantic " + pydantic_version) diff --git a/hdl21/datatype.py b/hdl21/datatype.py index 1f0cbfa6..f8c9e020 100644 --- a/hdl21/datatype.py +++ b/hdl21/datatype.py @@ -27,15 +27,53 @@ - Notable exceptions include *union types* thereof, which do not have the necessary fields/ methods. """ -from pydantic import Extra -from pydantic.dataclasses import dataclass from typing import TypeVar, Type, Optional +from pydantic import __version__ as _pydantic_version + + +_pydantic_major_version = int(_pydantic_version.split(".")[0]) +if _pydantic_major_version > 2 or _pydantic_major_version < 1: + msg = "Error reading Pydantic version. Should be either 1.x or 2.x." + raise ImportError(msg) + +if _pydantic_major_version == 1: + from pydantic import Extra, BaseModel + from pydantic.json import pydantic_encoder as pydantic_json_encoder + + PYDANTIC_V2 = False + + class OurBaseConfig: + allow_extra = Extra.forbid + + class AllowArbConfig(OurBaseConfig): + arbitrary_types_allowed = True + + def _update_forward_refs(): + """Update all the forward type-references""" + for tp in datatypes: + tp.__pydantic_model__.update_forward_refs() + +else: # _pydantic_major_version==2 + from pydantic import Extra, BaseModel, RootModel, BeforeValidator + from pydantic.deprecated.json import pydantic_encoder as pydantic_json_encoder + + PYDANTIC_V2 = True + OurBaseConfig = dict(allow_extra="forbid", validate_default=True) + AllowArbConfig = dict( + allow_extra="forbid", validate_default=True, arbitrary_types_allowed=True + ) + + def _update_forward_refs(): + """Update all the forward type-references""" + ... + # for tp in datatypes: + # tp.model_rebuild() -# The list of defined datatypes -datatypes = [] +from pydantic.dataclasses import dataclass T = TypeVar("T") +datatypes = [] # The list of defined datatypes def _datatype(cls: Type[T], *, config: Optional[Type] = None, **kwargs) -> Type[T]: @@ -63,22 +101,3 @@ def datatype(cls: Optional[Type[T]] = None, **kwargs) -> Type[T]: if cls is None: return inner # Called with parens, e.g. `@datatype()` return inner(cls) # Called without parens - - -def _update_forward_refs(): - """Update all the forward type-references""" - for tp in datatypes: - tp.__pydantic_model__.update_forward_refs() - - -""" -# Define a few common pydantic model `Config`s -""" - - -class OurBaseConfig: - allow_extra = Extra.forbid - - -class AllowArbConfig(OurBaseConfig): - arbitrary_types_allowed = True diff --git a/hdl21/elab/passes/flatten_bundles.py b/hdl21/elab/passes/flatten_bundles.py index 2e24ada4..9b1835ec 100644 --- a/hdl21/elab/passes/flatten_bundles.py +++ b/hdl21/elab/passes/flatten_bundles.py @@ -90,9 +90,6 @@ def add_subscope(self, name: str, scope: "BundleScope"): self.signals[path_from_self] = sig -BundleScope.__pydantic_model__.update_forward_refs() - - @datatype(config=AllowArbConfig) class BundlePortEntry: """# Bundle-Port Entry in the Cache diff --git a/hdl21/external_module.py b/hdl21/external_module.py index 1b357811..300a1afd 100644 --- a/hdl21/external_module.py +++ b/hdl21/external_module.py @@ -54,6 +54,8 @@ def Params(self) -> Type: return self.paramtype def __post_init__(self): + """Post-Constructor Checks""" + # Check for a valid parameter-type if not isparamclass(self.paramtype) and self.paramtype not in (dict, Dict): msg = f"Invalid parameter type {self.paramtype} for {self}. " @@ -64,8 +66,7 @@ def __post_init__(self): self._source_info: Optional[SourceInfo] = source_info(get_pymodule=True) self._importpath = None - def __post_init_post_parse__(self): - """After type-checking, do some more checks on values""" + # Now do some more checks on values for p in self.port_list: if not p.name: raise ValueError(f"Unnamed Primitive Port {p} for {self.name}") @@ -99,7 +100,7 @@ class ExternalModuleCall: module: ExternalModule params: Any - def __post_init_post_parse__(self): + def __post_init__(self): # Type-validate our parameters if not isinstance(self.params, self.module.paramtype): msg = f"Invalid parameter type {type(self.params)} for ExternalModule {self.module.name}. Must be {self.module.paramtype}" diff --git a/hdl21/flatten.py b/hdl21/flatten.py index 323fdfa9..d55960e8 100644 --- a/hdl21/flatten.py +++ b/hdl21/flatten.py @@ -37,7 +37,7 @@ class FlattenedInstance: path: List[h.Instance] = field(default_factory=list) conns: Dict[str, h.Signal] = field(default_factory=dict) - def __post_init_post_parse__(self): + def __post_init__(self): # Assert that this instance's target is either a primitive, or external if not isinstance(self.inst.of, (h.PrimitiveCall, h.ExternalModuleCall)): raise ValueError(f"Invalid flattened instance {self}") diff --git a/hdl21/instantiable.py b/hdl21/instantiable.py index b68af6fc..c4de21b1 100644 --- a/hdl21/instantiable.py +++ b/hdl21/instantiable.py @@ -3,12 +3,13 @@ and thus supports its "connect by call" and "connect by assignment" semantics. """ -import copy +# Std-Lib Imports +from __future__ import annotations from typing import Any, Union, Dict +import copy -from pydantic import BaseModel - -from .datatype import AllowArbConfig +# Local Imports +from .datatype import AllowArbConfig, _pydantic_major_version from .module import Module from .primitives import PrimitiveCall from .external_module import ExternalModuleCall @@ -18,31 +19,7 @@ InstantiableUnion = Union[Module, ExternalModuleCall, PrimitiveCall] -class Instantiable(BaseModel): - """ - # Instantiable - - Generally this means - ````python - Union[Module, ExternalModuleCall, PrimitiveCall] - ``` - with some customized checking and error handling. - """ - - __root__: InstantiableUnion - Config = AllowArbConfig - - def __init__(self, *_, **__): - # Brick any attempts to create instances - msg = f"Invalid attempt to instantiate an `Instantiable` directly. " - raise RuntimeError(msg) - - @classmethod - def __get_validators__(cls): - yield assert_instantiable - - -def assert_instantiable(i: Any) -> Instantiable: +def assert_instantiable(i: Any) -> "Instantiable": """# Assert that `i` is an `Instantiable` type.""" if not is_instantiable(i): return invalid(i) @@ -74,7 +51,7 @@ def invalid(val: Any) -> None: raise TypeError(msg) -def qualname(i: Instantiable) -> str: +def qualname(i: "Instantiable") -> str: """Path-qualified name of Instantiable `i`""" from .qualname import qualname as module_qualname @@ -91,7 +68,7 @@ def qualname(i: Instantiable) -> str: raise TypeError(f"Invalid Instantiable {i}") -def io(i: Instantiable) -> Dict[str, "Connectable"]: +def io(i: "Instantiable") -> Dict[str, "Connectable"]: """ Get a complete dictionary of IO ports for `i`, including all types: Signals and Bundles. Copies the Instantiable's top-level dictionary so that it is not modified by consumers. @@ -103,4 +80,41 @@ def io(i: Instantiable) -> Dict[str, "Connectable"]: return rv +_doc = """ +# Instantiable + +Generally this means +````python +Union[Module, ExternalModuleCall, PrimitiveCall] +``` +with some customized checking and error handling. +""" + +if _pydantic_major_version == 1: + from .datatype import BaseModel + + class Instantiable(BaseModel): + # "Custom root types" implementation + + __doc__ = _doc + __root__: InstantiableUnion + Config = AllowArbConfig + + def __init__(self, *_, **__): + # Brick any attempts to create instances + msg = f"Invalid attempt to instantiate an `Instantiable` directly. " + raise RuntimeError(msg) + + @classmethod + def __get_validators__(cls): + yield assert_instantiable + +else: + from .datatype import BeforeValidator + from typing import Annotated + + Instantiable = Annotated[InstantiableUnion, BeforeValidator(assert_instantiable)] + Instantiable.__doc__ = _doc + + __all__ = ["Instantiable", "is_instantiable"] diff --git a/hdl21/noconn.py b/hdl21/noconn.py index 6fd9b16c..7b40e442 100644 --- a/hdl21/noconn.py +++ b/hdl21/noconn.py @@ -33,7 +33,7 @@ class NoConn: name: Optional[str] = None - def __post_init_post_parse__(self) -> None: + def __post_init__(self) -> None: # Internal management data # Connected port references self._connected_ports: Set[PortRef] = set() diff --git a/hdl21/params.py b/hdl21/params.py index 6af35b35..60fbc942 100644 --- a/hdl21/params.py +++ b/hdl21/params.py @@ -11,7 +11,7 @@ # Local Imports from .default import Default -from .datatype import AllowArbConfig +from .datatype import AllowArbConfig, pydantic_json_encoder T = TypeVar("T") @@ -293,7 +293,7 @@ def hdl21_naming_encoder(obj: Any) -> Any: return {f.name: getattr(obj, f.name) for f in dataclasses.fields(obj)} # Not an Hdl21 type. Hand off to pydantic. - return pydantic.json.pydantic_encoder(obj) + return pydantic_json_encoder(obj) # Shortcut for parameter-less generators. diff --git a/hdl21/pdk/sample_pdk/pdk.py b/hdl21/pdk/sample_pdk/pdk.py index e036beb8..f91364e7 100644 --- a/hdl21/pdk/sample_pdk/pdk.py +++ b/hdl21/pdk/sample_pdk/pdk.py @@ -47,7 +47,7 @@ class SamplePdkMosParams: nf = h.Param(dtype=h.Scalar, desc="Number of parallel fingers", default=1) m = h.Param(dtype=h.Scalar, desc="Number of parallel fingers", default=1) - def __post_init_post_parse__(self): + def __post_init__(self): """Value Checks""" if self.w <= 0: raise ValueError(f"MosParams with invalid width {self.w}") diff --git a/hdl21/portref.py b/hdl21/portref.py index 7ee9e911..d7d0a1d0 100644 --- a/hdl21/portref.py +++ b/hdl21/portref.py @@ -24,7 +24,7 @@ class PortRef: inst: _Instance portname: str - def __post_init_post_parse__(self): + def __post_init__(self): # Inner management data self._connected_ports: Set[PortRef] = set() self.resolved: Union[None, "Signal", "BundleInstance"] = None diff --git a/hdl21/prefix.py b/hdl21/prefix.py index 84e284cd..527ae695 100644 --- a/hdl21/prefix.py +++ b/hdl21/prefix.py @@ -183,17 +183,20 @@ def new(cls, number: Decimal, prefix: Prefix = Prefix.UNIT) -> "Prefixed": for using arguments by-position, which `pydantic.BaseModel` does not support.""" return cls(number=number, prefix=prefix) - @classmethod - def validate(cls, v: Union["Prefixed", "ToPrefixed"]) -> "Prefixed": - """Validate `v` as a `Prefixed` number, or convert to `Prefixed` if applicable. - While usable elsewhere, `validate` is primarily intended for use in type-validated - dataclass trees, such as those generated in `paramclass`es.""" - - return to_prefixed(v) - - @classmethod - def __get_validators__(cls): - yield cls.validate + # + # FIXME: pending potential deprecation #157 + # + # @classmethod + # def validate(cls, v: Union["Prefixed", "ToPrefixed"]) -> "Prefixed": + # """Validate `v` as a `Prefixed` number, or convert to `Prefixed` if applicable. + # While usable elsewhere, `validate` is primarily intended for use in type-validated + # dataclass trees, such as those generated in `paramclass`es.""" + # + # return to_prefixed(v) + # + # @classmethod + # def __get_validators__(cls): + # yield cls.validate def __hash__(self): return hash((self.number, self.prefix)) @@ -335,7 +338,7 @@ def __ge__(self, other) -> bool: ToPrefixed = Union[int, float, str, Decimal] -def to_prefixed(v: Union[Prefixed, "ToPrefixed"]) -> Prefixed: +def to_prefixed(v: Union[Prefixed, ToPrefixed]) -> Prefixed: """Convert any convertible type to a `Prefixed` number.""" if isinstance(v, Prefixed): diff --git a/hdl21/primitives.py b/hdl21/primitives.py index 6e830968..c8a9ca1e 100644 --- a/hdl21/primitives.py +++ b/hdl21/primitives.py @@ -99,7 +99,7 @@ class Primitive: paramtype: Type[object] # Class/ Type of valid Parameters primtype: PrimitiveType # Ideal vs Physical Primitive-Type - def __post_init_post_parse__(self): + def __post_init__(self): """After type-checking, do plenty more checks on values""" if not isparamclass(self.paramtype): msg = f"Invalid Primitive param-type {self.paramtype} for {self.name}, must be an `hdl21.paramclass`" @@ -143,7 +143,7 @@ class PrimitiveCall: prim: Primitive params: Any = NoParams - def __post_init_post_parse__(self): + def __post_init__(self): # Type-validate our parameters if not isinstance(self.params, self.prim.paramtype): msg = f"Invalid parameters {self.params} for Primitive {self.prim}. Must be {self.prim.paramtype}" @@ -187,7 +187,8 @@ class PrimLibEntry: def _add(prim: Primitive, aliases: List[str]) -> Primitive: """Add a primitive to this library. Ensures its identifier matches its `name` field, and adds any aliases to the global namespace. - This is a private function and should be used solely during `hdl21.primitives` import-time.""" + This is a private function and should be used solely during `hdl21.primitives` import-time. + """ global _primitives if prim.name in _primitives or prim.name in globals(): @@ -255,7 +256,7 @@ class MosParams: family = Param(dtype=MosFamily, desc="Device family", default=MosFamily.NONE) model = Param(dtype=Optional[str], desc="Model (Name)", default=None) - # def __post_init_post_parse__(self): + # def __post_init__(self): # """Value Checks""" # # FIXME: re-introduce these, for the case in which the parameters are `Prefixed` and not `Literal` values. # if self.w <= 0: @@ -378,9 +379,9 @@ class IdealCapacitorParams: @paramclass class PhysicalCapacitorParams: - c = Param(dtype=Scalar, desc="Capacitance (F)", default=None) - w = Param(dtype=Scalar, desc="Width in resolution units", default=None) - l = Param(dtype=Scalar, desc="Length in resolution units", default=None) + w = Param(dtype=Optional[Scalar], desc="Width in resolution units", default=None) + l = Param(dtype=Optional[Scalar], desc="Length in resolution units", default=None) + c = Param(dtype=Optional[Scalar], desc="Capacitance (F)", default=None) model = Param(dtype=Optional[str], desc="Model (Name)", default=None) mult = Param(dtype=Optional[str], desc="Multiplier", default=None) @@ -654,7 +655,7 @@ class BipolarParams: model = Param(dtype=Optional[str], desc="Model (Name)", default=None) mult = Param(dtype=Optional[Scalar], desc="Multiplier", default=None) - def __post_init_post_parse__(self): + def __post_init__(self): """Value Checks""" if self.w is not None and self.w <= 0: raise ValueError(f"BipolarParams with invalid width {self.w}") diff --git a/hdl21/proto/exporting.py b/hdl21/proto/exporting.py index f936df8b..30ec23b0 100644 --- a/hdl21/proto/exporting.py +++ b/hdl21/proto/exporting.py @@ -373,9 +373,6 @@ def export_param_value(val: ToVlsirParam) -> Optional[vlsir.ParamValue]: return vlsir.ParamValue(literal=val.value) # Internal numeric (and number-like) types - if isinstance(val, Scalar): - # `Scalar` will either have an internal `Literal` or `Prefixed` value. - val = val.inner if isinstance(val, Literal): # String/ expression literals return vlsir.ParamValue(literal=val.text) if isinstance(val, Prefixed): diff --git a/hdl21/scalar.py b/hdl21/scalar.py index cd4a5548..ac549ff5 100644 --- a/hdl21/scalar.py +++ b/hdl21/scalar.py @@ -1,92 +1,79 @@ +# Std-Lib Imports +from __future__ import annotations from typing import Union from decimal import Decimal -from pydantic import BaseModel # Local Imports +from .datatype import _pydantic_major_version from .prefix import Prefixed from .literal import Literal -# Union of types convertible into `Scalar` -ToScalar = Union[Prefixed, Literal, str, int, float, Decimal] +# The shared docstring +_doc = """ +# The `Scalar` parameter type +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. -class Scalar(BaseModel): - """ - # The `Scalar` parameter type - - 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. - - `Scalar` is particularly designed for parameter-values of `Primitive`s and of simulations. - Most such parameters "want" to be the `Prefixed` type, for reasons outlined in - https://github.com/dan-fritchman/Hdl21#prefixed-numeric-parameters. - - They often also need a string-valued escape hatch, e.g. when referring to out-of-Hdl21 quantities - such as parameters in external netlists or simulation decks. - These out-of-Hdl21 expressions are represented by the `Literal` type, a simple wrapper around `str`. - - Where possible `Scalar` prefers to use the `Prefixed` variant. - Strings and built-in numbers (int, float, Decimal) are converted to `Prefixed` inline by the `validate` method. - All of the `validate` mechanisms work for `Scalar`s used as fields in `pydantic.dataclasses`. - which crucially include all `hdl21.paramclass`es. - - While defined as a type, `Scalar` is not instantiable. - It is really a class-based statement of `Union[Prefixed, Literal]`, with class methods to aid in validation. - - If writing "primitive-like" parameters - e.g. those that go into SPICE simulations, - or are provided to PDK-level devices, it is very likely that you will want to: - - * Use `Scalar` as a parameter type, i.e. the `dtype` field of `Param`s. - * Never actually instantiate a `Scalar` directly, including for its default value. - - Example: - - ```python - import hdl21 as h - from hdl21.prefix import NANO, µ - from decimal import Decimal - - @h.paramclass - class MyMosParams: - w = h.Param(dtype=h.Scalar, desc="Width", default=1e-6) # Default `float` converts to a `Prefixed` - l = h.Param(dtype=h.Scalar, desc="Length", default="w/5") # Default `str` converts to a `Literal` - - # Example instantiations - MyMosParams(w=Decimal(1e-6), l=3*µ) - MyMosParams(w=h.Literal("sim_param_width"), l=h.Prefixed.new(20, NANO)) - MyMosParams(w="11*l", l=11) - ``` - """ +`Scalar` is particularly designed for parameter-values of `Primitive`s and of simulations. +Most such parameters "want" to be the `Prefixed` type, for reasons outlined in +https://github.com/dan-fritchman/Hdl21#prefixed-numeric-parameters. + +They often also need a string-valued escape hatch, e.g. when referring to out-of-Hdl21 quantities +such as parameters in external netlists or simulation decks. +These out-of-Hdl21 expressions are represented by the `Literal` type, a simple wrapper around `str`. - # The Pydantic "custom root types" feature is really what makes this work: - # https://docs.pydantic.dev/latest/usage/models/#custom-root-types +Where possible `Scalar` prefers to use the `Prefixed` variant. +Strings and built-in numbers (int, float, Decimal) are converted to `Prefixed` inline by the `validate` method. +All of the `validate` mechanisms work for `Scalar`s used as fields in `pydantic.dataclasses`. +which crucially include all `hdl21.paramclass`es. - __root__: Union[Prefixed, Literal] +While defined as a type, `Scalar` is not instantiable. +It is really a class-based statement of `Union[Prefixed, Literal]`, with class methods to aid in validation. - def __init__(self, *_, **__): - # Brick any attempts to create instances - msg = f"Invalid attempt to instantiate a `Scalar` directly. " - msg += f"Create either of its variants `Prefixed` or `Literal` instead, " - msg += f"or use their built-in conversions from strings, ints, floats, and Decimals." - raise RuntimeError(msg) +If writing "primitive-like" parameters - e.g. those that go into SPICE simulations, +or are provided to PDK-level devices, it is very likely that you will want to: - @classmethod - def __get_validators__(cls): - yield to_scalar +* Use `Scalar` as a parameter type, i.e. the `dtype` field of `Param`s. +* Never actually instantiate a `Scalar` directly, including for its default value. + +Example: + +```python +import hdl21 as h +from hdl21.prefix import NANO, µ +from decimal import Decimal + +@h.paramclass +class MyMosParams: +w = h.Param(dtype=h.Scalar, desc="Width", default=1e-6) # Default `float` converts to a `Prefixed` +l = h.Param(dtype=h.Scalar, desc="Length", default="w/5") # Default `str` converts to a `Literal` + +# Example instantiations +MyMosParams(w=Decimal(1e-6), l=3*µ) +MyMosParams(w=h.Literal("sim_param_width"), l=h.Prefixed.new(20, NANO)) +MyMosParams(w="11*l", l=11) +``` +""" + + +# Union of types convertible into `Scalar` +ToScalar = Union[Prefixed, Literal, str, int, float, Decimal] def to_scalar(v: ToScalar) -> Union[Prefixed, Literal]: """# Validate and convert anything in the `ToScalar` set of types to a `Prefixed` or `Literal`. Most importantly this handles the case in which `v` is a *string*, - which attempts conversion to a `Prefixed`, and falls back to a `Literal` on failure.""" + which attempts conversion to a `Prefixed`, and falls back to a `Literal` on failure. + """ if isinstance(v, (Prefixed, Literal)): return v # Valid as-is, return it. @@ -102,5 +89,35 @@ def to_scalar(v: ToScalar) -> Union[Prefixed, Literal]: return Prefixed(number=v) +if _pydantic_major_version == 1: + from .datatype import BaseModel + + class Scalar(BaseModel): + # The Pydantic "custom root types" feature is really what makes this work: + # https://docs.pydantic.dev/latest/usage/models/#custom-root-types + __root__: Union[Prefixed, Literal] + __doc__ = _doc + + def __init__(self, *_, **__): + # Brick any attempts to create instances + msg = f"Invalid attempt to instantiate a `Scalar` directly. " + msg += f"Create either of its variants `Prefixed` or `Literal` instead, " + msg += f"or use their built-in conversions from strings, ints, floats, and Decimals." + raise RuntimeError(msg) + + @classmethod + def __get_validators__(cls): + yield to_scalar + +else: + from .datatype import BeforeValidator + from typing import Annotated + + # Union of types convertible into `Scalar` + Scalar = Annotated[ + Union[Prefixed, Literal], + BeforeValidator(to_scalar), + ] + __all__ = ["Scalar", "ToScalar", "to_scalar"] -__doc__ = Scalar.__doc__ +__doc__ = _doc diff --git a/hdl21/signal.py b/hdl21/signal.py index 1d428448..d2690dc3 100644 --- a/hdl21/signal.py +++ b/hdl21/signal.py @@ -106,7 +106,7 @@ class Signal: related_gnd: Optional["Signal"] = field(repr=False, default=None) # Related ground signal - def __post_init_post_parse__(self): + def __post_init__(self): if self.width < 1: raise ValueError(f"Signal {self.name} width must be positive") self._parent_module: Optional["Module"] = None diff --git a/hdl21/sim/data.py b/hdl21/sim/data.py index 68eafd07..94b2522c 100644 --- a/hdl21/sim/data.py +++ b/hdl21/sim/data.py @@ -207,7 +207,7 @@ def tp(self) -> AnalysisType: @simattr -@datatype +@datatype(config=AllowArbConfig) class SweepAnalysis: """Sweep over `inner` analyses""" @@ -222,7 +222,7 @@ def tp(self) -> AnalysisType: @simattr -@datatype +@datatype(config=AllowArbConfig) class MonteCarlo: """Add monte-carlo variations to one or more `inner` analyses.""" @@ -285,7 +285,7 @@ class Save: @simattr -@datatype +@datatype(config=AllowArbConfig) class Meas: """Measurement""" diff --git a/hdl21/sim/proto.py b/hdl21/sim/proto.py index beaa6f63..06288f0d 100644 --- a/hdl21/sim/proto.py +++ b/hdl21/sim/proto.py @@ -361,8 +361,6 @@ def export_float(num: Union[float, int, Decimal, Prefixed, Scalar]) -> float: return 0.0 if isinstance(num, float): return num - if isinstance(num, Scalar): - return float(num.inner) if isinstance(num, (int, str, Decimal, Prefixed)): return float(num) raise TypeError(f"Invalid value for proto float: {num}") diff --git a/hdl21/slice.py b/hdl21/slice.py index 62a6ea83..fd5c5243 100644 --- a/hdl21/slice.py +++ b/hdl21/slice.py @@ -43,7 +43,7 @@ class Slice: # Python index, i.e. that passed to square brackets index: Union[int, slice] - def __post_init_post_parse__(self): + def __post_init__(self): if not is_sliceable(self.parent): raise TypeError(f"{self.parent} is not Sliceable") self._connected_ports: Set["PortRef"] = set() diff --git a/hdl21/source_info.py b/hdl21/source_info.py index 2967ea38..f9532a06 100644 --- a/hdl21/source_info.py +++ b/hdl21/source_info.py @@ -26,17 +26,11 @@ from pathlib import Path from types import ModuleType, FrameType from typing import Optional -from pydantic import Extra -from .datatype import datatype +from .datatype import datatype, AllowArbConfig -class Config: - arbitrary_types_allowed = True - allow_extra = Extra.forbid - - -@datatype(config=Config) +@datatype(config=AllowArbConfig) class SourceInfo: """# Python Source Info""" diff --git a/hdl21/tests/test_params.py b/hdl21/tests/test_params.py index 294a0f5c..e0384071 100644 --- a/hdl21/tests/test_params.py +++ b/hdl21/tests/test_params.py @@ -117,8 +117,9 @@ class C: class D(C): ... - with pytest.raises(TypeError): + with pytest.raises((TypeError, ValidationError)): # Test that missing arguments fail + # Note whether this is `TypeError` or `ValidationError` depends on the version of Pydantic. c = C() with pytest.raises(ValidationError): diff --git a/hdl21/tests/test_prefix.py b/hdl21/tests/test_prefix.py index 4f9d8159..9cc91494 100644 --- a/hdl21/tests/test_prefix.py +++ b/hdl21/tests/test_prefix.py @@ -4,6 +4,7 @@ from pydantic.dataclasses import dataclass import hdl21 as h +from hdl21.datatype import OurBaseConfig def test_decimal(): @@ -132,7 +133,6 @@ def test_prefixed_mul(): def test_prefixed_div(): - """Test `Prefixed` True Division""" from hdl21.prefix import e @@ -478,10 +478,11 @@ def test_not_implemented_exponent(): assert e([]) == NotImplemented +@pt.mark.xfail(reason="Testing for #157 pydantic v2") def test_prefixed_and_scalar_conversions(): """Test inline conversions of built-in numeric types to `Prefixed` and `Scalar`.""" - @dataclass + @dataclass(config=OurBaseConfig) class P: x: h.Prefixed y: h.Scalar diff --git a/pdks/Gf180/gf180_hdl21/pdk_logic.py b/pdks/Gf180/gf180_hdl21/pdk_logic.py index 7f9ab556..23235f9a 100644 --- a/pdks/Gf180/gf180_hdl21/pdk_logic.py +++ b/pdks/Gf180/gf180_hdl21/pdk_logic.py @@ -285,13 +285,10 @@ def scale_param(self, orig: Optional[h.Scalar], default: h.Prefixed) -> h.Scalar Primarily type-dispatches across the need to scale to microns for this PDK.""" if orig is None: return default - if not isinstance(orig, h.Scalar): - orig = h.scalar.to_scalar(orig) - if isinstance(orig, h.Prefixed): - return orig + return orig # FIXME: where's the scaling? if isinstance(orig, h.Literal): - return h.Literal(f"({orig} * 1e6)") + return h.Literal(f"({orig.text} * 1e6)") raise TypeError(f"Param Value {orig}") def use_defaults(self, params: h.paramclass, modname: str, defaults: dict): diff --git a/pdks/Gf180/gf180_hdl21/test_pdk.py b/pdks/Gf180/gf180_hdl21/test_pdk.py index ffef48b5..1cda1793 100644 --- a/pdks/Gf180/gf180_hdl21/test_pdk.py +++ b/pdks/Gf180/gf180_hdl21/test_pdk.py @@ -143,7 +143,7 @@ def _compile_and_test(prims: h.Module, paramtype: h.Param): # ... and Test for k in prims.namespace: - if k is not "z": + if k != "z": assert isinstance(prims.namespace[k], h.Instance) assert isinstance(prims.namespace[k].of, h.ExternalModuleCall) diff --git a/pdks/Sky130/sky130_hdl21/pdk_data.py b/pdks/Sky130/sky130_hdl21/pdk_data.py index 640da9d9..54729974 100644 --- a/pdks/Sky130/sky130_hdl21/pdk_data.py +++ b/pdks/Sky130/sky130_hdl21/pdk_data.py @@ -1,17 +1,11 @@ # Std-Lib Imports from copy import deepcopy -from dataclasses import field -from typing import Dict, Tuple, List -from types import SimpleNamespace - -# PyPi Imports -from pydantic.dataclasses import dataclass +from typing import List # Hdl21 Imports import hdl21 as h from hdl21.prefix import ( MILLI, - µ, MEGA, TERA, ) @@ -35,7 +29,6 @@ # Vlsirtool Types to ease downstream parsing from vlsirtools import SpiceType -FIXME = None # FIXME: Replace with real values! PDK_NAME = "sky130" """ @@ -133,23 +126,23 @@ class MosParams: sa = h.Param( dtype=h.Scalar, desc="Spacing between Adjacent Gate to Drain", - default=h.Literal(0), + default=0, ) sb = h.Param( dtype=h.Scalar, desc="Spacing between Adjacent Gate to Source", - default=h.Literal(0), + default=0, ) sd = h.Param( dtype=h.Scalar, desc="Spacing between Adjacent Drain to Source", - default=h.Literal(0), + default=0, ) mult = h.Param(dtype=h.Scalar, desc="Multiplier", default=1) m = h.Param(dtype=h.Scalar, desc="Multiplier", default=1) -# FIXME: keep this alias as prior versions may have used it +# NOTE: probably add a deprecation note for this alias; prior versions may have used it Sky130MosParams = MosParams diff --git a/pdks/Sky130/sky130_hdl21/pdk_logic.py b/pdks/Sky130/sky130_hdl21/pdk_logic.py index 7a51a9bf..b9f04715 100644 --- a/pdks/Sky130/sky130_hdl21/pdk_logic.py +++ b/pdks/Sky130/sky130_hdl21/pdk_logic.py @@ -353,13 +353,10 @@ def scale_param(self, orig: Optional[h.Scalar], default: h.Prefixed) -> h.Scalar Primarily type-dispatches across the need to scale to microns for this PDK.""" if orig is None: return default - if not isinstance(orig, h.Scalar): - orig = h.scalar.to_scalar(orig) - if isinstance(orig, h.Prefixed): - return orig + return orig # FIXME: where's the scaling? if isinstance(orig, h.Literal): - return h.Literal(f"({orig} * 1e6)") + return h.Literal(f"({orig.text} * 1e6)") raise TypeError(f"Param Value {orig}") def use_defaults(self, params: h.paramclass, modname: str, defaults: dict): diff --git a/pdks/Sky130/sky130_hdl21/primitives/prim_dicts.py b/pdks/Sky130/sky130_hdl21/primitives/prim_dicts.py index b1301611..2485d678 100644 --- a/pdks/Sky130/sky130_hdl21/primitives/prim_dicts.py +++ b/pdks/Sky130/sky130_hdl21/primitives/prim_dicts.py @@ -1,8 +1,3 @@ -from ..pdk_data import * - -# Individuate component types -MosKey = Tuple[str, MosType, MosVth, MosFamily] - """ These dictionaries are used to map all of the devices of the Sky130 technology to their corresponding caller functions above. Keys and names are used to @@ -10,6 +5,18 @@ to find and determine the correct internal device to use. """ +# Std-Lib Imports +from typing import Tuple, Dict +from dataclasses import dataclass, field + +# Local Imports +from hdl21.prefix import µ +from ..pdk_data import * + +# Individuate component types +MosKey = Tuple[str, MosType, MosVth, MosFamily] + + xtors: Dict[MosKey, h.ExternalModule] = { # Add all generic transistors ("NMOS_1p8V_STD", MosType.NMOS, MosVth.STD, MosFamily.CORE): xtor_module( diff --git a/pdks/Sky130/sky130_hdl21/primitives/primitives.py b/pdks/Sky130/sky130_hdl21/primitives/primitives.py index 57832389..23880d34 100644 --- a/pdks/Sky130/sky130_hdl21/primitives/primitives.py +++ b/pdks/Sky130/sky130_hdl21/primitives/primitives.py @@ -1,5 +1,3 @@ -from ..pdk_data import * - """ These dictionaries are used to map all of the devices of the Sky130 technology to their corresponding caller functions above. Keys and names are used to @@ -7,6 +5,8 @@ to find and determine the correct internal device to use. """ +from ..pdk_data import * + NMOS_1p8V_STD = xtor_module("sky130_fd_pr__nfet_01v8") NMOS_1p8V_LOW = xtor_module("sky130_fd_pr__nfet_01v8_lvt") PMOS_1p8V_STD = xtor_module("sky130_fd_pr__pfet_01v8") diff --git a/setup.py b/setup.py index e2f8b533..5765415d 100644 --- a/setup.py +++ b/setup.py @@ -28,13 +28,13 @@ author_email="dan@fritch.mn", packages=find_packages(), package_data={"hdl21": ["**/*.sp"]}, # Include built-in PDK models - python_requires=">=3.7, <3.12", + python_requires=">=3.7, <3.13", install_requires=[ f"vlsir=={_VLSIR_VERSION}", f"vlsirtools=={_VLSIR_VERSION}", # Our primary external dependency is pydantic. - # Tested with everything in the 1.9-1.10 range. - "pydantic>=1.9.0,<1.11", + # Tested with everything in the 1.9-2.6 range. + "pydantic>=1.9.0,<2.7", ], extras_require={ "dev": [