From 20a5e3073b99c125db4fbce9e2630c65317aa5e5 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 2 Mar 2025 17:26:50 -0500 Subject: [PATCH] Remove pydantic and switch to dataclasses/validobj --- pyproject.toml | 2 +- robotpy_build/autowrap/context.py | 2 +- robotpy_build/autowrap/cxxparser.py | 21 +- robotpy_build/autowrap/generator_data.py | 24 ++- robotpy_build/autowrap/render_tmpl_inst.py | 2 +- robotpy_build/config/autowrap_yml.py | 218 ++++++++------------- robotpy_build/config/dev_yml.py | 6 +- robotpy_build/config/pyproject_toml.py | 79 ++++---- robotpy_build/config/util.py | 65 +++++- robotpy_build/setup.py | 11 +- tests/cpp/gen/ft/buffers.yml | 16 +- tests/cpp/gen/ft/overloads.yml | 35 ++++ tests/cpp/rpytest/ft/include/overloads.h | 5 + 13 files changed, 276 insertions(+), 210 deletions(-) create mode 100644 tests/cpp/gen/ft/overloads.yml diff --git a/pyproject.toml b/pyproject.toml index 0e00c638..191151b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "setuptools >= 45", "setuptools_scm >= 6.2, < 8", "sphinxify >= 0.7.3", - "pydantic >= 1.7.0, < 2, != 1.10.20", + "validobj ~= 1.2", "cxxheaderparser[pcpp] ~= 1.4.1", "tomli", "tomli_w", diff --git a/robotpy_build/autowrap/context.py b/robotpy_build/autowrap/context.py index c42079fc..94afdd5d 100644 --- a/robotpy_build/autowrap/context.py +++ b/robotpy_build/autowrap/context.py @@ -494,7 +494,7 @@ class TemplateInstanceContext: full_cpp_name_identifier: str binder_typename: str - params: typing.List[str] + params: typing.List[typing.Union[int, str]] header_name: str diff --git a/robotpy_build/autowrap/cxxparser.py b/robotpy_build/autowrap/cxxparser.py index d2d77c82..c0aadd07 100644 --- a/robotpy_build/autowrap/cxxparser.py +++ b/robotpy_build/autowrap/cxxparser.py @@ -115,13 +115,13 @@ def _gen_int_types(): _rvp_map = { - ReturnValuePolicy.TAKE_OWNERSHIP: "py::return_value_policy::take_ownership", - ReturnValuePolicy.COPY: "py::return_value_policy::copy", - ReturnValuePolicy.MOVE: "py::return_value_policy::move", - ReturnValuePolicy.REFERENCE: "py::return_value_policy::reference", - ReturnValuePolicy.REFERENCE_INTERNAL: "py::return_value_policy::reference_internal", - ReturnValuePolicy.AUTOMATIC: "", - ReturnValuePolicy.AUTOMATIC_REFERENCE: "py::return_value_policy::automatic_reference", + ReturnValuePolicy.take_ownership: "py::return_value_policy::take_ownership", + ReturnValuePolicy.copy: "py::return_value_policy::copy", + ReturnValuePolicy.move: "py::return_value_policy::move", + ReturnValuePolicy.reference: "py::return_value_policy::reference", + ReturnValuePolicy.reference_internal: "py::return_value_policy::reference_internal", + ReturnValuePolicy.automatic: "", + ReturnValuePolicy.automatic_reference: "py::return_value_policy::automatic_reference", } # fmt: off @@ -938,7 +938,7 @@ def _on_class_field( else: py_name = prop_name - if propdata.access == PropAccess.AUTOMATIC: + if propdata.access == PropAccess.auto: # const variables can't be written if f.constexpr or getattr(f.type, "const", False): prop_readonly = True @@ -949,7 +949,7 @@ def _on_class_field( else: prop_readonly = _is_prop_readonly(f.type) else: - prop_readonly = propdata.access == PropAccess.READONLY + prop_readonly = propdata.access == PropAccess.readonly doc = self._process_doc(f.doxygen, propdata) @@ -2038,7 +2038,8 @@ def parse_header( break for param in tmpl_data.params: - visitor._add_user_type_caster(param) + if isinstance(param, str): + visitor._add_user_type_caster(param) # User typealias additions visitor._extract_typealias(user_cfg.typealias, hctx.user_typealias, set()) diff --git a/robotpy_build/autowrap/generator_data.py b/robotpy_build/autowrap/generator_data.py index 3100c408..dd68b1ff 100644 --- a/robotpy_build/autowrap/generator_data.py +++ b/robotpy_build/autowrap/generator_data.py @@ -6,6 +6,7 @@ AutowrapConfigYaml, PropData, FunctionData, + OverloadData, ) from .context import OverloadTracker @@ -40,6 +41,23 @@ class ClsReportData: functions: FnMissingData = dataclasses.field(default_factory=dict) +def _merge_overload(data: FunctionData, overload: OverloadData) -> FunctionData: + # merge overload information + # - create a dictionary that contains things that haven't changed + changes = {"overloads": {}} + for f in dataclasses.fields(OverloadData): + v = getattr(overload, f.name) + if f.default_factory is not dataclasses.MISSING: + default = f.default_factory() + else: + default = f.default + + if v != default: + changes[f.name] = v + + return dataclasses.replace(data, **changes) + + class GeneratorData: """ Used by the hooks to retrieve user-specified generation data, and @@ -141,11 +159,7 @@ def get_function_data( overload = data.overloads.get(signature) missing = overload is None if not missing and overload: - # merge overload information - data = data.dict(exclude_unset=True) - del data["overloads"] - data.update(overload.dict(exclude_unset=True)) - data = FunctionData(**data) + data = _merge_overload(data, overload) report_data.overloads[signature] = is_private or not missing report_data.tracker.add_overload() diff --git a/robotpy_build/autowrap/render_tmpl_inst.py b/robotpy_build/autowrap/render_tmpl_inst.py index 028233ba..b8bfe270 100644 --- a/robotpy_build/autowrap/render_tmpl_inst.py +++ b/robotpy_build/autowrap/render_tmpl_inst.py @@ -10,7 +10,7 @@ def render_template_inst_cpp( r = RenderBuffer() render_class_prologue(r, hctx) - tmpl_params = ", ".join(tmpl_data.params) + tmpl_params = ", ".join(str(p) for p in tmpl_data.params) r.write_trim( f""" diff --git a/robotpy_build/config/autowrap_yml.py b/robotpy_build/config/autowrap_yml.py index 24e6c6df..79e4b8c1 100644 --- a/robotpy_build/config/autowrap_yml.py +++ b/robotpy_build/config/autowrap_yml.py @@ -3,15 +3,17 @@ # to modify the generated files # +import dataclasses import enum -from typing import Dict, List, Tuple, Optional +from typing import Dict, List, Tuple, Optional, Union -from pydantic import validator, Field -from .util import Model, _generating_documentation import yaml +from .util import fix_yaml_dict, parse_input -class ParamData(Model): + +@dataclasses.dataclass(frozen=True) +class ParamData: """Various ways to modify parameters""" #: Set parameter name to this @@ -58,7 +60,8 @@ class BufferType(str, enum.Enum): INOUT = "inout" -class BufferData(Model): +@dataclasses.dataclass(frozen=True) +class BufferData: #: Indicates what type of python buffer is required type: BufferType @@ -80,34 +83,19 @@ class ReturnValuePolicy(enum.Enum): for what each of these values mean. """ - TAKE_OWNERSHIP = "take_ownership" - COPY = "copy" - MOVE = "move" - REFERENCE = "reference" - REFERENCE_INTERNAL = "reference_internal" - AUTOMATIC = "automatic" - AUTOMATIC_REFERENCE = "automatic_reference" + take_ownership = "take_ownership" + copy = "copy" + move = "move" + reference = "reference" + reference_internal = "reference_internal" + automatic = "automatic" + automatic_reference = "automatic_reference" -class FunctionData(Model): +@dataclasses.dataclass(frozen=True) +class OverloadData: """ - Customize the way the autogenerator binds a function. - - .. code-block:: yaml - - functions: - # for non-overloaded functions, just specify the name + customizations - name_of_non_overloaded_fn: - # add customizations for function here - - # For overloaded functions, specify the name, but each overload - # separately - my_overloaded_fn: - overloads: - int, int: - # customizations for `my_overloaded_fn(int, int)` - int, int, int: - # customizations for `my_overloaded_fn(int, int, int)` + .. seealso:: :class:`.FunctionData` """ #: If True, don't wrap this @@ -150,7 +138,7 @@ class FunctionData(Model): rename: Optional[str] = None #: Mechanism to override individual parameters - param_override: Dict[str, ParamData] = {} + param_override: Dict[str, ParamData] = dataclasses.field(default_factory=dict) #: If specified, put the function in a sub.pack.age subpackage: Optional[str] = None @@ -159,9 +147,7 @@ class FunctionData(Model): #: function is called. no_release_gil: Optional[bool] = None - buffers: List[BufferData] = [] - - overloads: Dict[str, "FunctionData"] = {} + buffers: List[BufferData] = dataclasses.field(default_factory=list) #: Adds py::keep_alive to the function. Overrides automatic #: keepalive support, which retains references passed to constructors. @@ -169,7 +155,7 @@ class FunctionData(Model): keepalive: Optional[List[Tuple[int, int]]] = None #: https://pybind11.readthedocs.io/en/stable/advanced/functions.html#return-value-policies - return_value_policy: ReturnValuePolicy = ReturnValuePolicy.AUTOMATIC + return_value_policy: ReturnValuePolicy = ReturnValuePolicy.automatic #: If this is a function template, this is a list of instantiations #: that you wish to provide. This is a list of lists, where the inner @@ -203,24 +189,30 @@ class FunctionData(Model): #: virtual_xform: Optional[str] = None - @validator("overloads", pre=True) - def validate_overloads(cls, value): - for k, v in value.items(): - if v is None: - value[k] = FunctionData() - return value - @validator("virtual_xform") - def validate_virtual_xform(cls, v, values): - if v and values.get("trampoline_cpp_code"): - raise ValueError( - "cannot specify trampoline_cpp_code and virtual_xform for the same method" - ) - return v +@dataclasses.dataclass(frozen=True) +class FunctionData(OverloadData): + """ + Customize the way the autogenerator binds a function. + + .. code-block:: yaml + + functions: + # for non-overloaded functions, just specify the name + customizations + name_of_non_overloaded_fn: + # add customizations for function here + # For overloaded functions, specify the name, but each overload + # separately + my_overloaded_fn: + overloads: + int, int: + # customizations for `my_overloaded_fn(int, int)` + int, int, int: + # customizations for `my_overloaded_fn(int, int, int)` + """ -if not _generating_documentation: - FunctionData.update_forward_refs() + overloads: Dict[str, OverloadData] = dataclasses.field(default_factory=dict) class PropAccess(enum.Enum): @@ -231,18 +223,19 @@ class PropAccess(enum.Enum): #: * If a struct/union, default to readwrite #: * If a class, default to readwrite if a basic type that isn't a #: reference, otherwise default to readonly - AUTOMATIC = "auto" + auto = "auto" #: Allow python users access to the value, but ensure it can't #: change. This is useful for properties that are defined directly #: in the class - READONLY = "readonly" + readonly = "readonly" #: Allows python users to read/write the value - READWRITE = "readwrite" + readwrite = "readwrite" -class PropData(Model): +@dataclasses.dataclass(frozen=True) +class PropData: #: If set to True, this property is not made available to python ignore: bool = False @@ -250,7 +243,7 @@ class PropData(Model): rename: Optional[str] = None #: Python code access to this property - access: PropAccess = PropAccess.AUTOMATIC + access: PropAccess = PropAccess.auto #: Docstring for the property (only available on class properties) doc: Optional[str] = None @@ -259,7 +252,8 @@ class PropData(Model): doc_append: Optional[str] = None -class EnumValue(Model): +@dataclasses.dataclass(frozen=True) +class EnumValue: #: If set to True, this property is not made available to python ignore: bool = False @@ -273,7 +267,8 @@ class EnumValue(Model): doc_append: Optional[str] = None -class EnumData(Model): +@dataclasses.dataclass(frozen=True) +class EnumData: #: Set your own docstring for the enum doc: Optional[str] = None @@ -293,7 +288,7 @@ class EnumData(Model): #: enums that are part of classes) subpackage: Optional[str] = None - values: Dict[str, EnumValue] = {} + values: Dict[str, EnumValue] = dataclasses.field(default_factory=dict) #: This will insert code right before the semicolon ending the enum py #: definition. You can use this to easily insert additional custom values @@ -306,7 +301,8 @@ class EnumData(Model): arithmetic: bool = False -class ClassData(Model): +@dataclasses.dataclass(frozen=True) +class ClassData: #: Docstring for the class doc: Optional[str] = None @@ -316,16 +312,16 @@ class ClassData(Model): ignore: bool = False #: List of bases to ignore. Name must include any template specializations. - ignored_bases: List[str] = [] + ignored_bases: List[str] = dataclasses.field(default_factory=list) #: Specify fully qualified names for the bases. If the base has a template #: parameter, you must include it. Only needed if it can't be automatically #: detected directly from the text. - base_qualnames: Dict[str, str] = {} + base_qualnames: Dict[str, str] = dataclasses.field(default_factory=dict) - attributes: Dict[str, PropData] = {} - enums: Dict[str, EnumData] = {} - methods: Dict[str, FunctionData] = {} + attributes: Dict[str, PropData] = dataclasses.field(default_factory=dict) + enums: Dict[str, EnumData] = dataclasses.field(default_factory=dict) + methods: Dict[str, FunctionData] = dataclasses.field(default_factory=dict) is_polymorphic: Optional[bool] = None force_no_trampoline: bool = False @@ -340,13 +336,13 @@ class ClassData(Model): #: If there are circular dependencies, this will help you resolve them #: manually. TODO: make it so we don't need this - force_depends: List[str] = [] + force_depends: List[str] = dataclasses.field(default_factory=list) #: Use this to bring in type casters for a particular type that may have #: been hidden (for example, with a typedef or definition in another file), #: instead of explicitly including the header. This should be the full #: namespace of the type. - force_type_casters: List[str] = [] + force_type_casters: List[str] = dataclasses.field(default_factory=list) #: If the object shouldn't be deleted by pybind11, use this. Disables #: implicit constructors. @@ -366,11 +362,11 @@ class ClassData(Model): #: Extra 'using' directives to insert into the trampoline and the #: wrapping scope - typealias: List[str] = [] + typealias: List[str] = dataclasses.field(default_factory=list) #: Fully-qualified pre-existing constant that will be inserted into the #: trampoline and wrapping scopes as a constexpr - constants: List[str] = [] + constants: List[str] = dataclasses.field(default_factory=list) #: If this is a template class, a list of the parameters if it can't #: be autodetected (currently can't autodetect). If there is no space @@ -400,29 +396,9 @@ class ClassData(Model): #: inline_code: Optional[str] = None - @validator("attributes", pre=True) - def validate_attributes(cls, value): - for k, v in value.items(): - if v is None: - value[k] = PropData() - return value - - @validator("enums", pre=True) - def validate_enums(cls, value): - for k, v in value.items(): - if v is None: - value[k] = EnumData() - return value - - @validator("methods", pre=True) - def validate_methods(cls, value): - for k, v in value.items(): - if v is None: - value[k] = FunctionData() - return value - - -class TemplateData(Model): + +@dataclasses.dataclass(frozen=True) +class TemplateData: """ Instantiates a template as a python type. To customize the class, add it to the ``classes`` key and specify the template type. @@ -455,7 +431,7 @@ class MyClass {}; qualname: str #: Template parameters to use - params: List[str] + params: List[Union[str, int]] #: If specified, put the template instantiation in a sub.pack.age subpackage: Optional[str] = None @@ -467,7 +443,8 @@ class MyClass {}; doc_append: Optional[str] = None -class Defaults(Model): +@dataclasses.dataclass(frozen=True) +class Defaults: """ Defaults to apply to everything """ @@ -480,24 +457,25 @@ class Defaults(Model): report_ignored_missing: bool = True -class AutowrapConfigYaml(Model): +@dataclasses.dataclass(frozen=True) +class AutowrapConfigYaml: """ Format of the file in [tool.robotpy-build.wrappers."PACKAGENAME"] generation_data """ - defaults: Defaults = Field(default_factory=Defaults) + defaults: Defaults = dataclasses.field(default_factory=Defaults) - strip_prefixes: List[str] = [] + strip_prefixes: List[str] = dataclasses.field(default_factory=list) #: Adds ``#include `` directives to the top of the autogenerated #: C++ file, after autodetected include dependencies are inserted. - extra_includes: List[str] = [] + extra_includes: List[str] = dataclasses.field(default_factory=list) #: Adds ``#include `` directives after robotpy_build.h is #: included, but before any autodetected include dependencies. Only use #: this when dealing with broken headers. - extra_includes_first: List[str] = [] + extra_includes_first: List[str] = dataclasses.field(default_factory=list) #: Specify raw C++ code that will be inserted at the end of the #: autogenerated file, inside a function. This is useful for extending @@ -530,7 +508,7 @@ class AutowrapConfigYaml(Model): #: my_variable: #: # customizations here, see PropData #: - attributes: Dict[str, PropData] = {} + attributes: Dict[str, PropData] = dataclasses.field(default_factory=dict) #: Key is the class name #: @@ -540,7 +518,7 @@ class AutowrapConfigYaml(Model): #: CLASSNAME: #: # customizations here, see ClassData #: - classes: Dict[str, ClassData] = {} + classes: Dict[str, ClassData] = dataclasses.field(default_factory=dict) #: Key is the function name #: @@ -550,7 +528,7 @@ class AutowrapConfigYaml(Model): #: fn_name: #: # customizations here, see FunctionData #: - functions: Dict[str, FunctionData] = {} + functions: Dict[str, FunctionData] = dataclasses.field(default_factory=dict) #: Key is the enum name, for enums at global scope #: @@ -560,7 +538,7 @@ class AutowrapConfigYaml(Model): #: MyEnum: #: # customizations here, see EnumData #: - enums: Dict[str, EnumData] = {} + enums: Dict[str, EnumData] = dataclasses.field(default_factory=dict) #: Instantiates a template. Key is the name to give to the Python type. #: @@ -570,43 +548,15 @@ class AutowrapConfigYaml(Model): #: ClassName: #: # customizations here, see TemplateData #: - templates: Dict[str, TemplateData] = {} + templates: Dict[str, TemplateData] = dataclasses.field(default_factory=dict) #: Extra 'using' directives to insert into the trampoline and the #: wrapping scope - typealias: List[str] = [] + typealias: List[str] = dataclasses.field(default_factory=list) #: Encoding to use when opening this header file encoding: str = "utf-8-sig" - @validator("attributes", pre=True) - def validate_attributes(cls, value): - for k, v in value.items(): - if v is None: - value[k] = PropData() - return value - - @validator("classes", pre=True) - def validate_classes(cls, value): - for k, v in value.items(): - if v is None: - value[k] = ClassData() - return value - - @validator("enums", pre=True) - def validate_enums(cls, value): - for k, v in value.items(): - if v is None: - value[k] = EnumData() - return value - - @validator("functions", pre=True) - def validate_functions(cls, value): - for k, v in value.items(): - if v is None: - value[k] = FunctionData() - return value - @classmethod def from_file(cls, fname) -> "AutowrapConfigYaml": with open(fname) as fp: @@ -615,4 +565,6 @@ def from_file(cls, fname) -> "AutowrapConfigYaml": if data is None: data = {} - return cls(**data) + data = fix_yaml_dict(data) + + return parse_input(data, cls, fname) diff --git a/robotpy_build/config/dev_yml.py b/robotpy_build/config/dev_yml.py index 15918759..869ab694 100644 --- a/robotpy_build/config/dev_yml.py +++ b/robotpy_build/config/dev_yml.py @@ -1,11 +1,11 @@ +import dataclasses import os from typing import Optional, List import yaml -from .util import Model - -class DevConfig(Model): +@dataclasses.dataclass +class DevConfig: """ Configuration options useful for developing robotpy-build wrappers. To use these, set the environment variable RPYBUILD_GEN_FILTER=filename.yml diff --git a/robotpy_build/config/pyproject_toml.py b/robotpy_build/config/pyproject_toml.py index 67898589..074391f8 100644 --- a/robotpy_build/config/pyproject_toml.py +++ b/robotpy_build/config/pyproject_toml.py @@ -2,16 +2,16 @@ # pyproject.toml # +import dataclasses import re from typing import Dict, List, Optional -from .util import Model - _arch_re = re.compile(r"\{\{\s*ARCH\s*\}\}") _os_re = re.compile(r"\{\{\s*OS\s*\}\}") -class PatchInfo(Model): +@dataclasses.dataclass +class PatchInfo: """ A unified diff to apply to downloaded source code before building a a wrapper. @@ -30,7 +30,8 @@ class PatchInfo(Model): strip: int = 0 -class MavenLibDownload(Model): +@dataclasses.dataclass +class MavenLibDownload: """ Used to download artifacts from a maven repository. This can download headers, shared libraries, and sources. @@ -78,10 +79,10 @@ class MavenLibDownload(Model): dlopenlibs: Optional[List[str]] = None #: Library extensions map - libexts: Dict[str, str] = {} + libexts: Dict[str, str] = dataclasses.field(default_factory=dict) #: Compile time extensions map - linkexts: Dict[str, str] = {} + linkexts: Dict[str, str] = dataclasses.field(default_factory=dict) #: If :attr:`use_sources` is set, this is the list of sources to compile sources: Optional[List[str]] = None @@ -94,7 +95,8 @@ class MavenLibDownload(Model): header_patches: Optional[List[PatchInfo]] = None -class Download(Model): +@dataclasses.dataclass +class Download: """ Download sources/libs/includes from a single file @@ -125,7 +127,7 @@ class Download(Model): #: Extra include paths, relative to the include directory #: #: {{ARCH}} and {{OS}} are replaced with the architecture/os name - extra_includes: List[str] = [] + extra_includes: List[str] = dataclasses.field(default_factory=list) # Common with MavenLibDownload @@ -137,10 +139,10 @@ class Download(Model): dlopenlibs: Optional[List[str]] = None #: Library extensions map - libexts: Dict[str, str] = {} + libexts: Dict[str, str] = dataclasses.field(default_factory=dict) #: Compile time extensions map - linkexts: Dict[str, str] = {} + linkexts: Dict[str, str] = dataclasses.field(default_factory=dict) #: List of sources to compile sources: Optional[List[str]] = None @@ -167,7 +169,8 @@ def _update_with_platform(self, platform): ] -class StaticLibConfig(Model): +@dataclasses.dataclass +class StaticLibConfig: """ Static libraries that can be consumed as a dependency by other wrappers in the same project. Static libraries are not directly installed, and @@ -191,7 +194,8 @@ class StaticLibConfig(Model): ignore: bool = False -class TypeCasterConfig(Model): +@dataclasses.dataclass +class TypeCasterConfig: """ Specifies type casters that this package exports. robotpy-build will attempt to detect these types at generation time and include @@ -220,7 +224,8 @@ class TypeCasterConfig(Model): default_arg_cast: bool = False -class WrapperConfig(Model): +@dataclasses.dataclass +class WrapperConfig: """ Configuration for building a C++ python extension module, optionally using autogenerated wrappers around existing library code. @@ -255,7 +260,7 @@ class WrapperConfig(Model): #: * It will be linked to any libraries the dependency contains #: * The python module for the dependency will be imported in the #: ``_init{extension}.py`` file. - depends: List[str] = [] + depends: List[str] = dataclasses.field(default_factory=list) #: If this project depends on external libraries stored in a maven repo #: specify it here. @@ -267,11 +272,11 @@ class WrapperConfig(Model): #: List of extra include directories to export, relative to the #: project root. - extra_includes: List[str] = [] + extra_includes: List[str] = dataclasses.field(default_factory=list) #: Optional source files to compile. Path is relative to the root of #: the project. - sources: List[str] = [] + sources: List[str] = dataclasses.field(default_factory=list) #: Specifies header files that autogenerated pybind11 wrappers will be #: created for. Simple C++ headers will most likely 'just work', but @@ -311,16 +316,17 @@ class WrapperConfig(Model): generation_data: Optional[str] = None #: Specifies type casters that this package exports. - type_casters: List[TypeCasterConfig] = [] + type_casters: List[TypeCasterConfig] = dataclasses.field(default_factory=list) #: Preprocessor definitions to apply when compiling this wrapper. - pp_defines: List[str] = [] + pp_defines: List[str] = dataclasses.field(default_factory=list) #: If True, skip this wrapper; typically used in conjection with an override. ignore: bool = False -class DistutilsMetadata(Model): +@dataclasses.dataclass +class DistutilsMetadata: """ Configures the metadata that robotpy-build passes to setuptools when the project is installed. The keys in this section match the standard @@ -348,16 +354,9 @@ class DistutilsMetadata(Model): .. note:: This section is required """ - class Config: - # allow passing in extra keywords to setuptools - extra = "allow" - #: The name of the package name: str - #: A single line describing the package - description: Optional[str] = None - #: The name of the package author author: str @@ -375,8 +374,14 @@ class Config: #: the requirement is set to be the same version as this package install_requires: List[str] + #: A single line describing the package + description: Optional[str] = None -class SupportedPlatform(Model): + entry_points: Optional[Dict[str, List[str]]] = dataclasses.field(default_factory=dict) + + +@dataclasses.dataclass +class SupportedPlatform: """ Supported platforms for this project. Currently this information is merely advisory, and is used to generate error messages when platform @@ -401,7 +406,8 @@ class SupportedPlatform(Model): arch: Optional[str] = None -class RobotpyBuildConfig(Model): +@dataclasses.dataclass +class RobotpyBuildConfig: """ Contains information for configuring the project @@ -416,26 +422,29 @@ class RobotpyBuildConfig(Model): #: Python package to store version information and robotpy-build metadata in base_package: str + #: project metadata + metadata: DistutilsMetadata + #: List of headers for the scan-headers tool to ignore - scan_headers_ignore: List[str] = [] + scan_headers_ignore: List[str] = dataclasses.field(default_factory=list) #: List of python packages with __init__.py to update when ``python setup.py update_init`` #: is called -- this is an argument to the ``robotpy-build create-imports`` command, and #: may contain a space and the second argument to create-imports. - update_init: List[str] = [] + update_init: List[str] = dataclasses.field(default_factory=list) #: #: .. seealso:: :class:`.SupportedPlatform` #: - supported_platforms: List[SupportedPlatform] = [] + supported_platforms: List[SupportedPlatform] = dataclasses.field( + default_factory=list + ) # # These are all documented in their class, it's more confusing to document # them here too. # - metadata: DistutilsMetadata - - wrappers: Dict[str, WrapperConfig] = {} + wrappers: Dict[str, WrapperConfig] = dataclasses.field(default_factory=dict) - static_libs: Dict[str, StaticLibConfig] = {} + static_libs: Dict[str, StaticLibConfig] = dataclasses.field(default_factory=dict) diff --git a/robotpy_build/config/util.py b/robotpy_build/config/util.py index 5caec00c..82d2afef 100644 --- a/robotpy_build/config/util.py +++ b/robotpy_build/config/util.py @@ -1,12 +1,59 @@ -import os -from pydantic import BaseModel +import typing -# Needed because pydantic gets in the way of generating good docs -_generating_documentation = bool(os.environ.get("GENERATING_DOCUMENTATION")) -if _generating_documentation: - BaseModel = object +from validobj import errors +import validobj.validation +T = typing.TypeVar("T") -class Model(BaseModel): - class Config: - extra = "forbid" + +class ValidationError(Exception): + pass + + +def _convert_validation_error( + fname: str, ve: errors.ValidationError +) -> ValidationError: + locs = [] + msg = [] + + e = ve + while e is not None: + + if isinstance(e, errors.WrongFieldError): + locs.append(f".{e.wrong_field}") + elif isinstance(e, errors.WrongListItemError): + locs.append(f"[{e.wrong_index}]") + else: + msg.append(str(e)) + + e = e.__cause__ + + loc = "".join(locs) + if loc.startswith("."): + loc = loc[1:] + msg = "\n ".join(msg) + vmsg = f"{fname}: {loc}:\n {msg}" + return ValidationError(vmsg) + + +def parse_input(value: typing.Any, spec: typing.Type[T], fname: str) -> T: + try: + return validobj.validation.parse_input(value, spec) + except errors.ValidationError as ve: + raise _convert_validation_error(fname, ve) from None + + +# yaml converts empty values to None, but we never want that +def fix_yaml_dict(a: typing.Any): + if isinstance(a, dict): + for k, v in a.items(): + if v is None: + a[k] = {} + if isinstance(v, dict): + fix_yaml_dict(v) + elif isinstance(a, list): + for v in a: + if isinstance(v, dict): + fix_yaml_dict(v) + + return a diff --git a/robotpy_build/setup.py b/robotpy_build/setup.py index 75912c29..81242a6f 100644 --- a/robotpy_build/setup.py +++ b/robotpy_build/setup.py @@ -30,6 +30,7 @@ def finalize_options(self): except ImportError: EditableWheel = None # type: ignore +from .config.util import parse_input from .config.pyproject_toml import RobotpyBuildConfig from .maven import convert_maven_to_downloads @@ -63,14 +64,16 @@ def __init__(self): self.project_dict = self.pyproject.get("tool", {}).get("robotpy-build", {}) - # Overrides are applied before pydantic does processing, so that - # we can easily override anything without needing to make the - # pydantic schemas messy with needless details + # Overrides are applied before processing, so that we can easily + # override anything without needing to make the dataclasses messy + # with needless details override_keys = get_platform_override_keys(self.platform) apply_overrides(self.project_dict, override_keys) try: - self.project = RobotpyBuildConfig(**self.project_dict) + self.project = parse_input( + self.project_dict, RobotpyBuildConfig, project_fname + ) except Exception as e: raise ValueError( f"robotpy-build configuration in pyproject.toml is incorrect" diff --git a/tests/cpp/gen/ft/buffers.yml b/tests/cpp/gen/ft/buffers.yml index 10ee3590..cc4181f6 100644 --- a/tests/cpp/gen/ft/buffers.yml +++ b/tests/cpp/gen/ft/buffers.yml @@ -5,24 +5,24 @@ classes: methods: set_buffer: buffers: - - { type: in, src: data, len: len } + - { type: IN, src: data, len: len } get_buffer2: buffers: - - { type: out, src: data, len: len } + - { type: OUT, src: data, len: len } get_buffer1: buffers: - - { type: out, src: data, len: len } + - { type: OUT, src: data, len: len } inout_buffer: buffers: - - { type: in, src: indata, len: size } - - { type: out, src: outdata, len: size } + - { type: IN, src: indata, len: size } + - { type: OUT, src: outdata, len: size } v_set_buffer: buffers: - - { type: in, src: data, len: len } + - { type: IN, src: data, len: len } v_get_buffer2: buffers: - - { type: out, src: data, len: len } + - { type: OUT, src: data, len: len } v_get_buffer1: buffers: - - { type: out, src: data, len: len } \ No newline at end of file + - { type: OUT, src: data, len: len } \ No newline at end of file diff --git a/tests/cpp/gen/ft/overloads.yml b/tests/cpp/gen/ft/overloads.yml new file mode 100644 index 00000000..6e7bedf8 --- /dev/null +++ b/tests/cpp/gen/ft/overloads.yml @@ -0,0 +1,35 @@ +--- + +functions: + fnOverload: + overloads: + int, int: + int: +classes: + OverloadedObject: + methods: + overloaded: + overloads: + int: + const char*: + int, int: + int, int, int: + param_override: + a: + name: x + b: + name: y + c: + name: z + "": + overloaded_constexpr: + overloads: + int, int: + int, int, int: + overloaded_static: + overloads: + int: + const char*: + overloaded_private: + overloads: + int: diff --git a/tests/cpp/rpytest/ft/include/overloads.h b/tests/cpp/rpytest/ft/include/overloads.h index b0cfdce3..836a10db 100644 --- a/tests/cpp/rpytest/ft/include/overloads.h +++ b/tests/cpp/rpytest/ft/include/overloads.h @@ -29,6 +29,11 @@ struct OverloadedObject return o; } + // checking that param override works + int overloaded(int a, int b, int c) { + return a + b + c; + } + // This shows rtnType is inconsistent in CppHeaderParser const OverloadedObject& overloaded() { return *this;