From 6ca6262172b369a4cba0e3b8442cbf5d3de1f36b Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Thu, 30 Apr 2026 17:41:38 +0200 Subject: [PATCH 1/9] Make flow360 root import lazy --- flow360/__init__.py | 430 ++++-------------------------------- flow360/_api.py | 394 +++++++++++++++++++++++++++++++++ flow360/cli/api_set_func.py | 28 +-- flow360/user_config.py | 65 +++++- tests/test_lazy_imports.py | 71 +++++- 5 files changed, 564 insertions(+), 424 deletions(-) create mode 100644 flow360/_api.py diff --git a/flow360/__init__.py b/flow360/__init__.py index cc5c63346..9950c9067 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -1,404 +1,56 @@ """ -This module is flow360 for simulation based models +This module is flow360 for simulation based models. """ +from __future__ import annotations + +import ast from importlib import import_module +from pathlib import Path +from types import ModuleType + +_API_MODULE: ModuleType | None = None + + +def _load_api_module() -> ModuleType: + global _API_MODULE # pylint: disable=global-statement + if _API_MODULE is None: + _API_MODULE = import_module("flow360._api") + return _API_MODULE -from flow360_schema.framework.expression import ( - UserVariable, - get_user_variable, - remove_user_variable, - show_user_variables, -) -from flow360_schema.models.functions import math -from flow360_schema.models.variables import solution -from flow360.accounts_utils import Accounts -from flow360.cli.api_set_func import configure_caller as configure -from flow360.component.case import Case -from flow360.component.cloud_examples import show_available_examples -from flow360.component.geometry import Geometry -from flow360.component.project import Project, create_draft -from flow360.component.simulation import migration, services -from flow360.component.simulation import units as u -from flow360.component.simulation.draft_context.mirror import MirrorPlane -from flow360.component.simulation.entity_operation import CoordinateSystem -from flow360.component.simulation.folder import Folder -from flow360.component.simulation.framework.entity_selector import ( - BodyGroupSelector, - EdgeSelector, - VolumeSelector, -) -from flow360.component.simulation.meshing_param import snappy -from flow360.component.simulation.meshing_param.edge_params import ( - AngleBasedRefinement, - AspectRatioBasedRefinement, - HeightBasedRefinement, - ProjectAnisoSpacing, - SurfaceEdgeRefinement, -) -from flow360.component.simulation.meshing_param.face_params import ( - BoundaryLayer, - GeometryRefinement, - PassiveSpacing, - SurfaceRefinement, -) -from flow360.component.simulation.meshing_param.meshing_specs import ( - MeshingDefaults, - OctreeSpacing, - VolumeMeshingDefaults, -) -from flow360.component.simulation.meshing_param.params import ( - MeshingParams, - ModularMeshingWorkflow, - VolumeMeshingParams, -) -from flow360.component.simulation.meshing_param.volume_params import ( - AutomatedFarfield, - AxisymmetricRefinement, - CentralBelt, - CustomZones, - FullyMovingFloor, - MeshSliceOutput, - RotationCylinder, - RotationSphere, - RotationVolume, - StaticFloor, - StructuredBoxRefinement, - UniformRefinement, - UserDefinedFarfield, - WheelBelts, - WindTunnelFarfield, -) -from flow360.component.simulation.models.material import ( - Air, - FrozenSpecies, - NASA9Coefficients, - NASA9CoefficientSet, - SolidMaterial, - Sutherland, - ThermallyPerfectGas, - Water, -) -from flow360.component.simulation.models.solver_numerics import ( - DetachedEddySimulation, - HeatEquationSolver, - KOmegaSST, - KOmegaSSTModelConstants, - KrylovLinearSolver, - LinearSolver, - LineSearch, - NavierStokesSolver, - NoneSolver, - SpalartAllmaras, - SpalartAllmarasModelConstants, - TransitionModelSolver, - TurbulenceModelControls, -) -from flow360.component.simulation.models.surface_models import ( - Freestream, - HeatFlux, - Inflow, - Mach, - MassFlowRate, - Outflow, - Periodic, - PorousJump, - Pressure, - Rotational, - SlaterPorousBleed, - SlipWall, - Supersonic, - SymmetryPlane, - Temperature, - TotalPressure, - Translational, - Wall, - WallFunction, - WallRotation, -) -from flow360.component.simulation.models.turbulence_quantities import ( - TurbulenceQuantities, -) -from flow360.component.simulation.models.volume_models import ( - ActuatorDisk, - AngleExpression, - AngularVelocity, - BETDisk, - BETDiskChord, - BETDiskSectionalPolar, - BETDiskTwist, - C81File, - DFDCFile, - Fluid, - ForcePerArea, - FromUserDefinedDynamics, - Gravity, - HeatEquationInitialCondition, - NavierStokesInitialCondition, - NavierStokesModifiedRestartSolution, - PorousMedium, - Rotation, - Solid, - XFOILFile, - XROTORFile, -) -from flow360.component.simulation.operating_condition.operating_condition import ( - AerospaceCondition, - GenericReferenceCondition, - LiquidOperatingCondition, - ThermalState, -) -from flow360.component.simulation.outputs import render_config -from flow360.component.simulation.outputs.output_entities import ( - Isosurface, - Point, - PointArray, - PointArray2D, - Slice, -) -from flow360.component.simulation.outputs.outputs import ( - AeroAcousticOutput, - ForceDistributionOutput, - ForceOutput, - IsosurfaceOutput, - MovingStatistic, - Observer, - ProbeOutput, - RenderOutput, - RenderOutputGroup, - SliceOutput, - StreamlineOutput, - SurfaceIntegralOutput, - SurfaceOutput, - SurfaceProbeOutput, - SurfaceSliceOutput, - TimeAverageForceDistributionOutput, - TimeAverageIsosurfaceOutput, - TimeAverageProbeOutput, - TimeAverageSliceOutput, - TimeAverageStreamlineOutput, - TimeAverageSurfaceOutput, - TimeAverageSurfaceProbeOutput, - TimeAverageVolumeOutput, - UserDefinedField, - VolumeOutput, -) -from flow360.component.simulation.primitives import ( - AxisymmetricBody, - Box, - CustomVolume, - Cylinder, - ReferenceGeometry, - SeedpointVolume, - Sphere, -) -from flow360.component.simulation.run_control.run_control import RunControl -from flow360.component.simulation.run_control.stopping_criterion import ( - StoppingCriterion, -) -from flow360.component.simulation.simulation_params import SimulationParams -from flow360.component.simulation.time_stepping.time_stepping import ( - AdaptiveCFL, - RampCFL, - Steady, - Unsteady, -) -from flow360.component.simulation.unit_system import ( - CGS_unit_system, - SI_unit_system, - imperial_unit_system, -) -from flow360.component.simulation.user_defined_dynamics.user_defined_dynamics import ( - UserDefinedDynamic, -) -from flow360.component.surface_mesh_v2 import SurfaceMeshV2 as SurfaceMesh -from flow360.component.volume_mesh import VolumeMeshV2 as VolumeMesh -from flow360.environment import Env -from flow360.plugins import report -from flow360.version_utils import warn_if_prerelease_version as _warn_prerelease +def _load_exported_names() -> list[str]: + api_source = Path(__file__).with_name("_api.py").read_text(encoding="utf-8") + module_ast = ast.parse(api_source) + for node in module_ast.body: + if not isinstance(node, ast.Assign): + continue + for target in node.targets: + if isinstance(target, ast.Name) and target.id == "__all__": + return ast.literal_eval(node.value) + return [] -__all__ = [ - "GeometryRefinement", - "Env", - "Case", - "create_draft", - "CoordinateSystem", - "MirrorPlane", - "EdgeSelector", - "VolumeSelector", - "BodyGroupSelector", - "AngleBasedRefinement", - "AspectRatioBasedRefinement", - "ProjectAnisoSpacing", - "BoundaryLayer", - "PassiveSpacing", - "Accounts", - "Project", - "u", - "MeshSliceOutput", - "SimulationParams", - "SI_unit_system", - "imperial_unit_system", - "CGS_unit_system", - "services", - "MeshingParams", - "MeshingDefaults", - "SurfaceRefinement", - "AutomatedFarfield", - "AxisymmetricRefinement", - "CustomZones", - "StructuredBoxRefinement", - "RotationCylinder", - "RotationSphere", - "RotationVolume", - "UniformRefinement", - "SurfaceEdgeRefinement", - "HeightBasedRefinement", - "ReferenceGeometry", - "CustomVolume", - "Cylinder", - "Sphere", - "AxisymmetricBody", - "AerospaceCondition", - "ThermalState", - "LiquidOperatingCondition", - "Steady", - "Unsteady", - "RampCFL", - "AdaptiveCFL", - "Wall", - "WallFunction", - "Freestream", - "SlipWall", - "Outflow", - "Inflow", - "Periodic", - "PorousJump", - "SymmetryPlane", - "Fluid", - "Solid", - "ActuatorDisk", - "AngularVelocity", - "BETDisk", - "BETDiskChord", - "BETDiskSectionalPolar", - "BETDiskTwist", - "Rotation", - "PorousMedium", - "SurfaceOutput", - "TimeAverageSurfaceOutput", - "VolumeOutput", - "TimeAverageVolumeOutput", - "ForceDistributionOutput", - "TimeAverageForceDistributionOutput", - "SliceOutput", - "TimeAverageSliceOutput", - "IsosurfaceOutput", - "TimeAverageIsosurfaceOutput", - "SurfaceIntegralOutput", - "ProbeOutput", - "SurfaceProbeOutput", - "AeroAcousticOutput", - "StreamlineOutput", - "TimeAverageStreamlineOutput", - "Observer", - "HeatEquationSolver", - "NavierStokesSolver", - "NoneSolver", - "SpalartAllmaras", - "KOmegaSST", - "SpalartAllmarasModelConstants", - "DetachedEddySimulation", - "KOmegaSSTModelConstants", - "KrylovLinearSolver", - "LineSearch", - "LinearSolver", - "Folder", - "ForcePerArea", - "Air", - "NASA9CoefficientSet", - "NASA9Coefficients", - "FrozenSpecies", - "Sutherland", - "ThermallyPerfectGas", - "SolidMaterial", - "Slice", - "Isosurface", - "TurbulenceQuantities", - "UserDefinedDynamic", - "Translational", - "NavierStokesInitialCondition", - "NavierStokesModifiedRestartSolution", - "FromUserDefinedDynamics", - "Gravity", - "HeatEquationInitialCondition", - "Temperature", - "HeatFlux", - "Point", - "PointArray", - "AngleExpression", - "Box", - "GenericReferenceCondition", - "TransitionModelSolver", - "TurbulenceModelControls", - "Pressure", - "TotalPressure", - "Supersonic", - "Rotational", - "Mach", - "MassFlowRate", - "UserDefinedField", - "VolumeMesh", - "SurfaceMesh", - "UserDefinedFarfield", - "Geometry", - "XROTORFile", - "DFDCFile", - "C81File", - "XFOILFile", - "TimeAverageProbeOutput", - "TimeAverageSurfaceProbeOutput", - "SurfaceSliceOutput", - "SlaterPorousBleed", - "migration", - "Water", - "PointArray2D", - "WallRotation", - "UserVariable", - "math", - "solution", - "report", - "snappy", - "ModularMeshingWorkflow", - "SeedpointVolume", - "VolumeMeshingParams", - "VolumeMeshingDefaults", - "get_user_variable", - "show_user_variables", - "remove_user_variable", - "StoppingCriterion", - "MovingStatistic", - "OctreeSpacing", - "RunControl", - "WindTunnelFarfield", - "StaticFloor", - "FullyMovingFloor", - "CentralBelt", - "WheelBelts", - "show_available_examples", - "ForceOutput", - "RenderOutput", - "RenderOutputGroup", - "render_config", -] -_warn_prerelease() +__all__ = _load_exported_names() -def __getattr__(name): +def __getattr__(name: str): if name == "version_check": module = import_module("flow360.version_check") globals()[name] = module return module + + if name in __all__: + api_module = _load_api_module() + try: + value = getattr(api_module, name) + except AttributeError as error: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from error + globals()[name] = value + return value + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__(): + return sorted(set(globals()) | set(__all__)) diff --git a/flow360/_api.py b/flow360/_api.py new file mode 100644 index 000000000..71501c7f4 --- /dev/null +++ b/flow360/_api.py @@ -0,0 +1,394 @@ +""" +This module is flow360 for simulation based models +""" + +from flow360.accounts_utils import Accounts +from flow360.component.case import Case +from flow360.component.cloud_examples import show_available_examples +from flow360.component.geometry import Geometry +from flow360.component.project import Project, create_draft +from flow360.component.simulation import migration, services +from flow360.component.simulation import units as u +from flow360.component.simulation.draft_context.mirror import MirrorPlane +from flow360.component.simulation.entity_operation import CoordinateSystem +from flow360.component.simulation.folder import Folder +from flow360.component.simulation.framework.entity_selector import ( + BodyGroupSelector, + EdgeSelector, + VolumeSelector, +) +from flow360.component.simulation.meshing_param import snappy +from flow360.component.simulation.meshing_param.edge_params import ( + AngleBasedRefinement, + AspectRatioBasedRefinement, + HeightBasedRefinement, + ProjectAnisoSpacing, + SurfaceEdgeRefinement, +) +from flow360.component.simulation.meshing_param.face_params import ( + BoundaryLayer, + GeometryRefinement, + PassiveSpacing, + SurfaceRefinement, +) +from flow360.component.simulation.meshing_param.meshing_specs import ( + MeshingDefaults, + OctreeSpacing, + VolumeMeshingDefaults, +) +from flow360.component.simulation.meshing_param.params import ( + MeshingParams, + ModularMeshingWorkflow, + VolumeMeshingParams, +) +from flow360.component.simulation.meshing_param.volume_params import ( + AutomatedFarfield, + AxisymmetricRefinement, + CentralBelt, + CustomZones, + FullyMovingFloor, + MeshSliceOutput, + RotationCylinder, + RotationSphere, + RotationVolume, + StaticFloor, + StructuredBoxRefinement, + UniformRefinement, + UserDefinedFarfield, + WheelBelts, + WindTunnelFarfield, +) +from flow360.component.simulation.models.material import ( + Air, + FrozenSpecies, + NASA9Coefficients, + NASA9CoefficientSet, + SolidMaterial, + Sutherland, + ThermallyPerfectGas, + Water, +) +from flow360.component.simulation.models.solver_numerics import ( + DetachedEddySimulation, + HeatEquationSolver, + KOmegaSST, + KOmegaSSTModelConstants, + KrylovLinearSolver, + LinearSolver, + LineSearch, + NavierStokesSolver, + NoneSolver, + SpalartAllmaras, + SpalartAllmarasModelConstants, + TransitionModelSolver, + TurbulenceModelControls, +) +from flow360.component.simulation.models.surface_models import ( + Freestream, + HeatFlux, + Inflow, + Mach, + MassFlowRate, + Outflow, + Periodic, + PorousJump, + Pressure, + Rotational, + SlaterPorousBleed, + SlipWall, + Supersonic, + SymmetryPlane, + Temperature, + TotalPressure, + Translational, + Wall, + WallFunction, + WallRotation, +) +from flow360.component.simulation.models.turbulence_quantities import ( + TurbulenceQuantities, +) +from flow360.component.simulation.models.volume_models import ( + ActuatorDisk, + AngleExpression, + AngularVelocity, + BETDisk, + BETDiskChord, + BETDiskSectionalPolar, + BETDiskTwist, + C81File, + DFDCFile, + Fluid, + ForcePerArea, + FromUserDefinedDynamics, + Gravity, + HeatEquationInitialCondition, + NavierStokesInitialCondition, + NavierStokesModifiedRestartSolution, + PorousMedium, + Rotation, + Solid, + XFOILFile, + XROTORFile, +) +from flow360.component.simulation.operating_condition.operating_condition import ( + AerospaceCondition, + GenericReferenceCondition, + LiquidOperatingCondition, + ThermalState, +) +from flow360.component.simulation.outputs import render_config +from flow360.component.simulation.outputs.output_entities import ( + Isosurface, + Point, + PointArray, + PointArray2D, + Slice, +) +from flow360.component.simulation.outputs.outputs import ( + AeroAcousticOutput, + ForceDistributionOutput, + ForceOutput, + IsosurfaceOutput, + MovingStatistic, + Observer, + ProbeOutput, + RenderOutput, + RenderOutputGroup, + SliceOutput, + StreamlineOutput, + SurfaceIntegralOutput, + SurfaceOutput, + SurfaceProbeOutput, + SurfaceSliceOutput, + TimeAverageForceDistributionOutput, + TimeAverageIsosurfaceOutput, + TimeAverageProbeOutput, + TimeAverageSliceOutput, + TimeAverageStreamlineOutput, + TimeAverageSurfaceOutput, + TimeAverageSurfaceProbeOutput, + TimeAverageVolumeOutput, + UserDefinedField, + VolumeOutput, +) +from flow360.component.simulation.primitives import ( + AxisymmetricBody, + Box, + CustomVolume, + Cylinder, + ReferenceGeometry, + SeedpointVolume, + Sphere, +) +from flow360.component.simulation.run_control.run_control import RunControl +from flow360.component.simulation.run_control.stopping_criterion import ( + StoppingCriterion, +) +from flow360.component.simulation.simulation_params import SimulationParams +from flow360.component.simulation.time_stepping.time_stepping import ( + AdaptiveCFL, + RampCFL, + Steady, + Unsteady, +) +from flow360.component.simulation.unit_system import ( + CGS_unit_system, + SI_unit_system, + imperial_unit_system, +) +from flow360_schema.framework.expression import ( + UserVariable, + get_user_variable, + remove_user_variable, + show_user_variables, +) +from flow360_schema.models.functions import math +from flow360_schema.models.variables import solution +from flow360.component.simulation.user_defined_dynamics.user_defined_dynamics import ( + UserDefinedDynamic, +) +from flow360.component.surface_mesh_v2 import SurfaceMeshV2 as SurfaceMesh +from flow360.component.volume_mesh import VolumeMeshV2 as VolumeMesh +from flow360.environment import Env +from flow360.plugins import report +from flow360.user_config import configure_apikey as configure +from flow360.version_utils import warn_if_prerelease_version as _warn_prerelease + +__all__ = [ + "GeometryRefinement", + "Env", + "configure", + "Case", + "create_draft", + "CoordinateSystem", + "MirrorPlane", + "EdgeSelector", + "VolumeSelector", + "BodyGroupSelector", + "AngleBasedRefinement", + "AspectRatioBasedRefinement", + "ProjectAnisoSpacing", + "BoundaryLayer", + "PassiveSpacing", + "Accounts", + "Project", + "u", + "MeshSliceOutput", + "SimulationParams", + "SI_unit_system", + "imperial_unit_system", + "CGS_unit_system", + "services", + "MeshingParams", + "MeshingDefaults", + "SurfaceRefinement", + "AutomatedFarfield", + "AxisymmetricRefinement", + "CustomZones", + "StructuredBoxRefinement", + "RotationCylinder", + "RotationSphere", + "RotationVolume", + "UniformRefinement", + "SurfaceEdgeRefinement", + "HeightBasedRefinement", + "ReferenceGeometry", + "CustomVolume", + "Cylinder", + "Sphere", + "AxisymmetricBody", + "AerospaceCondition", + "ThermalState", + "LiquidOperatingCondition", + "Steady", + "Unsteady", + "RampCFL", + "AdaptiveCFL", + "Wall", + "WallFunction", + "Freestream", + "SlipWall", + "Outflow", + "Inflow", + "Periodic", + "PorousJump", + "SymmetryPlane", + "Fluid", + "Solid", + "ActuatorDisk", + "AngularVelocity", + "BETDisk", + "BETDiskChord", + "BETDiskSectionalPolar", + "BETDiskTwist", + "Rotation", + "PorousMedium", + "SurfaceOutput", + "TimeAverageSurfaceOutput", + "VolumeOutput", + "TimeAverageVolumeOutput", + "ForceDistributionOutput", + "TimeAverageForceDistributionOutput", + "SliceOutput", + "TimeAverageSliceOutput", + "IsosurfaceOutput", + "TimeAverageIsosurfaceOutput", + "SurfaceIntegralOutput", + "ProbeOutput", + "SurfaceProbeOutput", + "AeroAcousticOutput", + "StreamlineOutput", + "TimeAverageStreamlineOutput", + "Observer", + "HeatEquationSolver", + "NavierStokesSolver", + "NoneSolver", + "SpalartAllmaras", + "KOmegaSST", + "SpalartAllmarasModelConstants", + "DetachedEddySimulation", + "KOmegaSSTModelConstants", + "KrylovLinearSolver", + "LineSearch", + "LinearSolver", + "Folder", + "ForcePerArea", + "Air", + "NASA9CoefficientSet", + "NASA9Coefficients", + "FrozenSpecies", + "Sutherland", + "ThermallyPerfectGas", + "SolidMaterial", + "Slice", + "Isosurface", + "TurbulenceQuantities", + "UserDefinedDynamic", + "Translational", + "NavierStokesInitialCondition", + "NavierStokesModifiedRestartSolution", + "FromUserDefinedDynamics", + "Gravity", + "HeatEquationInitialCondition", + "Temperature", + "HeatFlux", + "Point", + "PointArray", + "AngleExpression", + "Box", + "GenericReferenceCondition", + "TransitionModelSolver", + "TurbulenceModelControls", + "Pressure", + "TotalPressure", + "Supersonic", + "Rotational", + "Mach", + "MassFlowRate", + "UserDefinedField", + "VolumeMesh", + "SurfaceMesh", + "UserDefinedFarfield", + "Geometry", + "XROTORFile", + "DFDCFile", + "C81File", + "XFOILFile", + "TimeAverageProbeOutput", + "TimeAverageSurfaceProbeOutput", + "SurfaceSliceOutput", + "SlaterPorousBleed", + "migration", + "Water", + "PointArray2D", + "WallRotation", + "UserVariable", + "math", + "solution", + "report", + "snappy", + "ModularMeshingWorkflow", + "SeedpointVolume", + "VolumeMeshingParams", + "VolumeMeshingDefaults", + "get_user_variable", + "show_user_variables", + "remove_user_variable", + "StoppingCriterion", + "MovingStatistic", + "OctreeSpacing", + "RunControl", + "WindTunnelFarfield", + "StaticFloor", + "FullyMovingFloor", + "CentralBelt", + "WheelBelts", + "show_available_examples", + "ForceOutput", + "RenderOutput", + "RenderOutputGroup", + "render_config", +] + +_warn_prerelease() diff --git a/flow360/cli/api_set_func.py b/flow360/cli/api_set_func.py index b36345a16..6b6b32378 100644 --- a/flow360/cli/api_set_func.py +++ b/flow360/cli/api_set_func.py @@ -1,10 +1,6 @@ """Helper function to set up the API key for the user.""" -from click.testing import CliRunner - -import flow360.user_config as user_config # pylint: disable=consider-using-from-import -from flow360.cli.app import configure -from flow360.log import log +from flow360.user_config import configure_apikey def configure_caller(apikey: str, environment: str = None, profile: str = "default") -> None: @@ -19,24 +15,4 @@ def configure_caller(apikey: str, environment: str = None, profile: str = "defau Returns: None """ - runner = CliRunner() - - # Construct CLI arguments as a list - args = ["--apikey", apikey, "--profile", profile] - - if environment: - if environment.lower() in ("dev", "uat"): - args += ["--" + environment.lower()] - elif environment.lower() == "prod": - args += [] - else: - args += ["--env", environment] - - # Invoke the `configure` command - result = runner.invoke(configure, args) - - if result.exit_code != 0: - log.error(result.output if result.output else str(result.exception)) - else: - log.info("Configuration successful.") - user_config.UserConfig = user_config.BasicUserConfig() # Reload + configure_apikey(apikey=apikey, environment=environment, profile=profile) diff --git a/flow360/user_config.py b/flow360/user_config.py index 134a06f51..2aeafc0f4 100644 --- a/flow360/user_config.py +++ b/flow360/user_config.py @@ -17,6 +17,35 @@ CONFIG_FILE_MODE = 0o600 +def _merge_overwrite(old: dict, new: dict): + """Deep-merge dictionaries while overwriting conflicts from `new`.""" + + for key, value in new.items(): + if key in old and isinstance(old[key], dict) and isinstance(value, dict): + _merge_overwrite(old[key], value) + else: + old[key] = value + return old + + +def _normalize_storage_environment_name(environment: Optional[str]) -> Optional[str]: + """Normalize environment names used for config storage.""" + + if environment is None: + return None + + normalized = environment.strip() + if not normalized: + return None + + lowered = normalized.lower() + if lowered == prod.name: + return None + if lowered in ("dev", "uat"): + return lowered + return normalized + + def _ensure_permissions(path: str, mode: int): """Best-effort permission hardening for local config paths.""" try: @@ -55,20 +84,34 @@ def store_apikey( ): """Store an API key using the same config layout consumed by UserConfig.""" config = read_user_config() + environment_name = _normalize_storage_environment_name(environment_name) if environment_name in (None, "", prod.name): entry = {profile: {"apikey": apikey}} else: entry = {profile: {environment_name: {"apikey": apikey}}} - # Avoid importing CLI modules at import time because the wider package has lazy-import paths. - from flow360.cli import dict_utils # pylint: disable=import-outside-toplevel - - dict_utils.merge_overwrite(config, entry) + _merge_overwrite(config, entry) write_user_config(config) return config +def configure_apikey( + apikey: str, + environment: Optional[str] = None, + profile: str = DEFAULT_PROFILE, +) -> None: + """SDK-facing helper for storing an API key without going through the CLI app.""" + + store_apikey( + apikey, + profile=profile, + environment_name=_normalize_storage_environment_name(environment), + ) + reload_user_config() + log.info("Configuration successful.") + + def delete_apikey(profile: str = DEFAULT_PROFILE, environment_name: Optional[str] = None): """Delete a stored API key for the selected profile/environment if present.""" config = read_user_config() @@ -162,7 +205,7 @@ def apikey(self, env): # If other environment is used, check if the key exists key = key.get(env.name, None) if key is None: - log.warning(f"Cannot find api key associated with environment '{env.name}'.") + log.debug(f"No api key configured for environment '{env.name}'.") return None if key is None else key.get("apikey", "") def suppress_submit_warning(self): @@ -209,4 +252,16 @@ def enable_validation(self): self._do_validation = True +def reload_user_config(): + """Reload the shared user-config object in place when possible.""" + + global UserConfig # pylint: disable=global-statement + + if isinstance(UserConfig, BasicUserConfig): + BasicUserConfig.__init__(UserConfig) + else: + UserConfig = BasicUserConfig() + return UserConfig + + UserConfig = BasicUserConfig() diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index 75bb93771..23234dfc7 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -1,5 +1,7 @@ import sys +import types +import toml from click.testing import CliRunner @@ -12,7 +14,8 @@ def test_import_flow360_does_not_eagerly_import_heavy_dependencies(monkeypatch): import flow360 # pylint: disable=import-outside-toplevel,import-error assert "pandas" not in sys.modules - assert hasattr(flow360, "Env") + assert "flow360._api" not in sys.modules + assert "Env" in flow360.__all__ def test_import_flow360_cli_app_does_not_eagerly_import_sdk_command_modules(monkeypatch): @@ -21,7 +24,10 @@ def test_import_flow360_cli_app_does_not_eagerly_import_sdk_command_modules(monk "flow360.cli", "flow360.cli.app", "flow360.cli.project", + "flow360.cli.assets", + "flow360.cli.draft", "flow360.cli.folder", + "flow360.cli.wait", "flow360.cloud.flow360_requests", ): sys.modules.pop(module_name, None) @@ -29,7 +35,10 @@ def test_import_flow360_cli_app_does_not_eagerly_import_sdk_command_modules(monk import flow360.cli.app # pylint: disable=import-outside-toplevel,import-error,unused-import assert "flow360.cli.project" not in sys.modules + assert "flow360.cli.assets" not in sys.modules + assert "flow360.cli.draft" not in sys.modules assert "flow360.cli.folder" not in sys.modules + assert "flow360.cli.wait" not in sys.modules assert "flow360.cloud.flow360_requests" not in sys.modules @@ -39,18 +48,72 @@ def test_flow360_root_help_does_not_eagerly_import_sdk_command_modules(monkeypat "flow360.cli", "flow360.cli.app", "flow360.cli.project", + "flow360.cli.assets", + "flow360.cli.draft", "flow360.cli.folder", + "flow360.cli.wait", "flow360.cloud.flow360_requests", ): sys.modules.pop(module_name, None) - from flow360.cli import ( - flow360, # pylint: disable=import-outside-toplevel,import-error - ) + from flow360.cli import flow360 # pylint: disable=import-outside-toplevel,import-error result = CliRunner().invoke(flow360, ["--help"]) assert result.exit_code == 0 assert "flow360.cli.project" not in sys.modules + assert "flow360.cli.assets" not in sys.modules + assert "flow360.cli.draft" not in sys.modules assert "flow360.cli.folder" not in sys.modules + assert "flow360.cli.wait" not in sys.modules assert "flow360.cloud.flow360_requests" not in sys.modules + + +def test_sdk_configure_helper_does_not_import_cli_modules(monkeypatch, tmp_path): + monkeypatch.delenv("FLOW360_SUPPRESS_BETA_WARNING", raising=False) + config_path = tmp_path / "config.toml" + + for module_name in ("flow360.cli", "flow360.cli.app", "flow360.cli.api_set_func"): + sys.modules.pop(module_name, None) + + import flow360.user_config as user_config # pylint: disable=import-outside-toplevel,import-error + + monkeypatch.setattr(user_config, "config_file", str(config_path)) + + user_config.configure_apikey("test-key", environment="dev", profile="default") + + config = toml.loads(config_path.read_text()) + assert config["default"]["dev"]["apikey"] == "test-key" + assert "flow360.cli.app" not in sys.modules + assert "flow360.cli.api_set_func" not in sys.modules + + +def test_flow360_configure_is_exposed_without_importing_api_module(monkeypatch): + monkeypatch.delenv("FLOW360_SUPPRESS_BETA_WARNING", raising=False) + for module_name in ( + "flow360", + "flow360._api", + "flow360.cli", + "flow360.cli.app", + "flow360.cli.api_set_func", + ): + sys.modules.pop(module_name, None) + + import flow360 # pylint: disable=import-outside-toplevel,import-error + + assert "configure" in flow360.__all__ + assert "flow360._api" not in sys.modules + assert "flow360.cli.app" not in sys.modules + assert "flow360.cli.api_set_func" not in sys.modules + + +def test_flow360_version_check_legacy_lazy_attribute_does_not_import_api_module(monkeypatch): + for module_name in ("flow360", "flow360._api", "flow360.version_check"): + sys.modules.pop(module_name, None) + + import flow360 # pylint: disable=import-outside-toplevel,import-error + + sys.modules["flow360.version_check"] = types.ModuleType("flow360.version_check") + + assert flow360.version_check.__name__ == "flow360.version_check" + assert "flow360._api" not in sys.modules From 3f35fdf628e11b1e2b196c76751e528cf41fa749 Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Thu, 30 Apr 2026 21:29:58 +0200 Subject: [PATCH 2/9] Fix lazy import lint issues --- flow360/_api.py | 17 +++++++++-------- flow360/user_config.py | 10 ++++++---- tests/test_lazy_imports.py | 4 +++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/flow360/_api.py b/flow360/_api.py index 71501c7f4..9d361957c 100644 --- a/flow360/_api.py +++ b/flow360/_api.py @@ -2,6 +2,15 @@ This module is flow360 for simulation based models """ +from flow360_schema.framework.expression import ( + UserVariable, + get_user_variable, + remove_user_variable, + show_user_variables, +) +from flow360_schema.models.functions import math +from flow360_schema.models.variables import solution + from flow360.accounts_utils import Accounts from flow360.component.case import Case from flow360.component.cloud_examples import show_available_examples @@ -197,14 +206,6 @@ SI_unit_system, imperial_unit_system, ) -from flow360_schema.framework.expression import ( - UserVariable, - get_user_variable, - remove_user_variable, - show_user_variables, -) -from flow360_schema.models.functions import math -from flow360_schema.models.variables import solution from flow360.component.simulation.user_defined_dynamics.user_defined_dynamics import ( UserDefinedDynamic, ) diff --git a/flow360/user_config.py b/flow360/user_config.py index 2aeafc0f4..4677c08d6 100644 --- a/flow360/user_config.py +++ b/flow360/user_config.py @@ -257,10 +257,12 @@ def reload_user_config(): global UserConfig # pylint: disable=global-statement - if isinstance(UserConfig, BasicUserConfig): - BasicUserConfig.__init__(UserConfig) - else: - UserConfig = BasicUserConfig() + current_user_config = globals().get("UserConfig") + if isinstance(current_user_config, BasicUserConfig): + BasicUserConfig.__init__(current_user_config) + return current_user_config + + UserConfig = BasicUserConfig() return UserConfig diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index 23234dfc7..92c85c12f 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -56,7 +56,9 @@ def test_flow360_root_help_does_not_eagerly_import_sdk_command_modules(monkeypat ): sys.modules.pop(module_name, None) - from flow360.cli import flow360 # pylint: disable=import-outside-toplevel,import-error + from flow360.cli import ( + flow360, # pylint: disable=import-outside-toplevel,import-error + ) result = CliRunner().invoke(flow360, ["--help"]) From 33500d144ea9979efc80180bedc68bc10e10629d Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Thu, 30 Apr 2026 21:35:57 +0200 Subject: [PATCH 3/9] Isolate lazy import module mutations --- tests/test_lazy_imports.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index 92c85c12f..e1e8efdac 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -1,15 +1,17 @@ import sys -import types import toml from click.testing import CliRunner +def _unload_modules(monkeypatch, *module_names): + for module_name in module_names: + monkeypatch.delitem(sys.modules, module_name, raising=False) + + def test_import_flow360_does_not_eagerly_import_heavy_dependencies(monkeypatch): monkeypatch.delenv("FLOW360_SUPPRESS_BETA_WARNING", raising=False) - sys.modules.pop("flow360", None) - sys.modules.pop("flow360._api", None) - sys.modules.pop("pandas", None) + _unload_modules(monkeypatch, "flow360", "flow360._api", "pandas") import flow360 # pylint: disable=import-outside-toplevel,import-error @@ -20,7 +22,8 @@ def test_import_flow360_does_not_eagerly_import_heavy_dependencies(monkeypatch): def test_import_flow360_cli_app_does_not_eagerly_import_sdk_command_modules(monkeypatch): monkeypatch.delenv("FLOW360_SUPPRESS_BETA_WARNING", raising=False) - for module_name in ( + _unload_modules( + monkeypatch, "flow360.cli", "flow360.cli.app", "flow360.cli.project", @@ -29,8 +32,7 @@ def test_import_flow360_cli_app_does_not_eagerly_import_sdk_command_modules(monk "flow360.cli.folder", "flow360.cli.wait", "flow360.cloud.flow360_requests", - ): - sys.modules.pop(module_name, None) + ) import flow360.cli.app # pylint: disable=import-outside-toplevel,import-error,unused-import @@ -44,7 +46,8 @@ def test_import_flow360_cli_app_does_not_eagerly_import_sdk_command_modules(monk def test_flow360_root_help_does_not_eagerly_import_sdk_command_modules(monkeypatch): monkeypatch.delenv("FLOW360_SUPPRESS_BETA_WARNING", raising=False) - for module_name in ( + _unload_modules( + monkeypatch, "flow360.cli", "flow360.cli.app", "flow360.cli.project", @@ -53,8 +56,7 @@ def test_flow360_root_help_does_not_eagerly_import_sdk_command_modules(monkeypat "flow360.cli.folder", "flow360.cli.wait", "flow360.cloud.flow360_requests", - ): - sys.modules.pop(module_name, None) + ) from flow360.cli import ( flow360, # pylint: disable=import-outside-toplevel,import-error @@ -75,8 +77,7 @@ def test_sdk_configure_helper_does_not_import_cli_modules(monkeypatch, tmp_path) monkeypatch.delenv("FLOW360_SUPPRESS_BETA_WARNING", raising=False) config_path = tmp_path / "config.toml" - for module_name in ("flow360.cli", "flow360.cli.app", "flow360.cli.api_set_func"): - sys.modules.pop(module_name, None) + _unload_modules(monkeypatch, "flow360.cli", "flow360.cli.app", "flow360.cli.api_set_func") import flow360.user_config as user_config # pylint: disable=import-outside-toplevel,import-error @@ -92,14 +93,14 @@ def test_sdk_configure_helper_does_not_import_cli_modules(monkeypatch, tmp_path) def test_flow360_configure_is_exposed_without_importing_api_module(monkeypatch): monkeypatch.delenv("FLOW360_SUPPRESS_BETA_WARNING", raising=False) - for module_name in ( + _unload_modules( + monkeypatch, "flow360", "flow360._api", "flow360.cli", "flow360.cli.app", "flow360.cli.api_set_func", - ): - sys.modules.pop(module_name, None) + ) import flow360 # pylint: disable=import-outside-toplevel,import-error @@ -110,12 +111,9 @@ def test_flow360_configure_is_exposed_without_importing_api_module(monkeypatch): def test_flow360_version_check_legacy_lazy_attribute_does_not_import_api_module(monkeypatch): - for module_name in ("flow360", "flow360._api", "flow360.version_check"): - sys.modules.pop(module_name, None) + _unload_modules(monkeypatch, "flow360", "flow360._api", "flow360.version_check") import flow360 # pylint: disable=import-outside-toplevel,import-error - sys.modules["flow360.version_check"] = types.ModuleType("flow360.version_check") - assert flow360.version_check.__name__ == "flow360.version_check" assert "flow360._api" not in sys.modules From 999fcfb21fdcf8fae7af46b158bcb57327303a12 Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Thu, 30 Apr 2026 21:57:29 +0200 Subject: [PATCH 4/9] Avoid duplicate environment normalization --- flow360/user_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow360/user_config.py b/flow360/user_config.py index 4677c08d6..35be80c6f 100644 --- a/flow360/user_config.py +++ b/flow360/user_config.py @@ -106,7 +106,7 @@ def configure_apikey( store_apikey( apikey, profile=profile, - environment_name=_normalize_storage_environment_name(environment), + environment_name=environment, ) reload_user_config() log.info("Configuration successful.") From 74e837d6b9a05acebba5aa209517dc348a5069fa Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Fri, 1 May 2026 09:53:32 +0200 Subject: [PATCH 5/9] Add stub exports for lazy package facade --- flow360/__init__.pyi | 180 +++++++++++++++++++++++++++++++++++++ tests/test_lazy_imports.py | 37 ++++++++ 2 files changed, 217 insertions(+) create mode 100644 flow360/__init__.pyi diff --git a/flow360/__init__.pyi b/flow360/__init__.pyi new file mode 100644 index 000000000..aa09036ae --- /dev/null +++ b/flow360/__init__.pyi @@ -0,0 +1,180 @@ +"""Static exports for the lazy Flow360 package facade.""" + +from . import version_check as version_check +from ._api import Accounts as Accounts +from ._api import ActuatorDisk as ActuatorDisk +from ._api import AdaptiveCFL as AdaptiveCFL +from ._api import AeroAcousticOutput as AeroAcousticOutput +from ._api import AerospaceCondition as AerospaceCondition +from ._api import Air as Air +from ._api import AngleBasedRefinement as AngleBasedRefinement +from ._api import AngleExpression as AngleExpression +from ._api import AngularVelocity as AngularVelocity +from ._api import AspectRatioBasedRefinement as AspectRatioBasedRefinement +from ._api import AutomatedFarfield as AutomatedFarfield +from ._api import AxisymmetricBody as AxisymmetricBody +from ._api import AxisymmetricRefinement as AxisymmetricRefinement +from ._api import BETDisk as BETDisk +from ._api import BETDiskChord as BETDiskChord +from ._api import BETDiskSectionalPolar as BETDiskSectionalPolar +from ._api import BETDiskTwist as BETDiskTwist +from ._api import BodyGroupSelector as BodyGroupSelector +from ._api import BoundaryLayer as BoundaryLayer +from ._api import Box as Box +from ._api import C81File as C81File +from ._api import Case as Case +from ._api import CentralBelt as CentralBelt +from ._api import CGS_unit_system as CGS_unit_system +from ._api import CoordinateSystem as CoordinateSystem +from ._api import CustomVolume as CustomVolume +from ._api import CustomZones as CustomZones +from ._api import Cylinder as Cylinder +from ._api import DetachedEddySimulation as DetachedEddySimulation +from ._api import DFDCFile as DFDCFile +from ._api import EdgeSelector as EdgeSelector +from ._api import Env as Env +from ._api import Fluid as Fluid +from ._api import Folder as Folder +from ._api import ForceDistributionOutput as ForceDistributionOutput +from ._api import ForceOutput as ForceOutput +from ._api import ForcePerArea as ForcePerArea +from ._api import Freestream as Freestream +from ._api import FromUserDefinedDynamics as FromUserDefinedDynamics +from ._api import FrozenSpecies as FrozenSpecies +from ._api import FullyMovingFloor as FullyMovingFloor +from ._api import GenericReferenceCondition as GenericReferenceCondition +from ._api import Geometry as Geometry +from ._api import GeometryRefinement as GeometryRefinement +from ._api import Gravity as Gravity +from ._api import HeatEquationInitialCondition as HeatEquationInitialCondition +from ._api import HeatEquationSolver as HeatEquationSolver +from ._api import HeatFlux as HeatFlux +from ._api import HeightBasedRefinement as HeightBasedRefinement +from ._api import Inflow as Inflow +from ._api import Isosurface as Isosurface +from ._api import IsosurfaceOutput as IsosurfaceOutput +from ._api import KOmegaSST as KOmegaSST +from ._api import KOmegaSSTModelConstants as KOmegaSSTModelConstants +from ._api import KrylovLinearSolver as KrylovLinearSolver +from ._api import LinearSolver as LinearSolver +from ._api import LineSearch as LineSearch +from ._api import LiquidOperatingCondition as LiquidOperatingCondition +from ._api import Mach as Mach +from ._api import MassFlowRate as MassFlowRate +from ._api import MeshingDefaults as MeshingDefaults +from ._api import MeshingParams as MeshingParams +from ._api import MeshSliceOutput as MeshSliceOutput +from ._api import MirrorPlane as MirrorPlane +from ._api import ModularMeshingWorkflow as ModularMeshingWorkflow +from ._api import MovingStatistic as MovingStatistic +from ._api import NASA9Coefficients as NASA9Coefficients +from ._api import NASA9CoefficientSet as NASA9CoefficientSet +from ._api import NavierStokesInitialCondition as NavierStokesInitialCondition +from ._api import ( + NavierStokesModifiedRestartSolution as NavierStokesModifiedRestartSolution, +) +from ._api import NavierStokesSolver as NavierStokesSolver +from ._api import NoneSolver as NoneSolver +from ._api import Observer as Observer +from ._api import OctreeSpacing as OctreeSpacing +from ._api import Outflow as Outflow +from ._api import PassiveSpacing as PassiveSpacing +from ._api import Periodic as Periodic +from ._api import Point as Point +from ._api import PointArray as PointArray +from ._api import PointArray2D as PointArray2D +from ._api import PorousJump as PorousJump +from ._api import PorousMedium as PorousMedium +from ._api import Pressure as Pressure +from ._api import ProbeOutput as ProbeOutput +from ._api import Project as Project +from ._api import ProjectAnisoSpacing as ProjectAnisoSpacing +from ._api import RampCFL as RampCFL +from ._api import ReferenceGeometry as ReferenceGeometry +from ._api import RenderOutput as RenderOutput +from ._api import RenderOutputGroup as RenderOutputGroup +from ._api import Rotation as Rotation +from ._api import Rotational as Rotational +from ._api import RotationCylinder as RotationCylinder +from ._api import RotationSphere as RotationSphere +from ._api import RotationVolume as RotationVolume +from ._api import RunControl as RunControl +from ._api import SeedpointVolume as SeedpointVolume +from ._api import SI_unit_system as SI_unit_system +from ._api import SimulationParams as SimulationParams +from ._api import SlaterPorousBleed as SlaterPorousBleed +from ._api import Slice as Slice +from ._api import SliceOutput as SliceOutput +from ._api import SlipWall as SlipWall +from ._api import Solid as Solid +from ._api import SolidMaterial as SolidMaterial +from ._api import SpalartAllmaras as SpalartAllmaras +from ._api import SpalartAllmarasModelConstants as SpalartAllmarasModelConstants +from ._api import Sphere as Sphere +from ._api import StaticFloor as StaticFloor +from ._api import Steady as Steady +from ._api import StoppingCriterion as StoppingCriterion +from ._api import StreamlineOutput as StreamlineOutput +from ._api import StructuredBoxRefinement as StructuredBoxRefinement +from ._api import Supersonic as Supersonic +from ._api import SurfaceEdgeRefinement as SurfaceEdgeRefinement +from ._api import SurfaceIntegralOutput as SurfaceIntegralOutput +from ._api import SurfaceMesh as SurfaceMesh +from ._api import SurfaceOutput as SurfaceOutput +from ._api import SurfaceProbeOutput as SurfaceProbeOutput +from ._api import SurfaceRefinement as SurfaceRefinement +from ._api import SurfaceSliceOutput as SurfaceSliceOutput +from ._api import Sutherland as Sutherland +from ._api import SymmetryPlane as SymmetryPlane +from ._api import Temperature as Temperature +from ._api import ThermallyPerfectGas as ThermallyPerfectGas +from ._api import ThermalState as ThermalState +from ._api import ( + TimeAverageForceDistributionOutput as TimeAverageForceDistributionOutput, +) +from ._api import TimeAverageIsosurfaceOutput as TimeAverageIsosurfaceOutput +from ._api import TimeAverageProbeOutput as TimeAverageProbeOutput +from ._api import TimeAverageSliceOutput as TimeAverageSliceOutput +from ._api import TimeAverageStreamlineOutput as TimeAverageStreamlineOutput +from ._api import TimeAverageSurfaceOutput as TimeAverageSurfaceOutput +from ._api import TimeAverageSurfaceProbeOutput as TimeAverageSurfaceProbeOutput +from ._api import TimeAverageVolumeOutput as TimeAverageVolumeOutput +from ._api import TotalPressure as TotalPressure +from ._api import TransitionModelSolver as TransitionModelSolver +from ._api import Translational as Translational +from ._api import TurbulenceModelControls as TurbulenceModelControls +from ._api import TurbulenceQuantities as TurbulenceQuantities +from ._api import UniformRefinement as UniformRefinement +from ._api import Unsteady as Unsteady +from ._api import UserDefinedDynamic as UserDefinedDynamic +from ._api import UserDefinedFarfield as UserDefinedFarfield +from ._api import UserDefinedField as UserDefinedField +from ._api import UserVariable as UserVariable +from ._api import VolumeMesh as VolumeMesh +from ._api import VolumeMeshingDefaults as VolumeMeshingDefaults +from ._api import VolumeMeshingParams as VolumeMeshingParams +from ._api import VolumeOutput as VolumeOutput +from ._api import VolumeSelector as VolumeSelector +from ._api import Wall as Wall +from ._api import WallFunction as WallFunction +from ._api import WallRotation as WallRotation +from ._api import Water as Water +from ._api import WheelBelts as WheelBelts +from ._api import WindTunnelFarfield as WindTunnelFarfield +from ._api import XFOILFile as XFOILFile +from ._api import XROTORFile as XROTORFile +from ._api import configure as configure +from ._api import create_draft as create_draft +from ._api import get_user_variable as get_user_variable +from ._api import imperial_unit_system as imperial_unit_system +from ._api import math as math +from ._api import migration as migration +from ._api import remove_user_variable as remove_user_variable +from ._api import render_config as render_config +from ._api import report as report +from ._api import services as services +from ._api import show_available_examples as show_available_examples +from ._api import show_user_variables as show_user_variables +from ._api import snappy as snappy +from ._api import solution as solution +from ._api import u as u diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index e1e8efdac..cc0920d78 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -1,4 +1,6 @@ +import ast import sys +from pathlib import Path import toml from click.testing import CliRunner @@ -9,6 +11,18 @@ def _unload_modules(monkeypatch, *module_names): monkeypatch.delitem(sys.modules, module_name, raising=False) +def _load_api_all(): + api_source = Path(__file__).parents[1] / "flow360" / "_api.py" + module_ast = ast.parse(api_source.read_text(encoding="utf-8")) + for node in module_ast.body: + if not isinstance(node, ast.Assign): + continue + for target in node.targets: + if isinstance(target, ast.Name) and target.id == "__all__": + return ast.literal_eval(node.value) + raise AssertionError("flow360._api must define __all__") + + def test_import_flow360_does_not_eagerly_import_heavy_dependencies(monkeypatch): monkeypatch.delenv("FLOW360_SUPPRESS_BETA_WARNING", raising=False) _unload_modules(monkeypatch, "flow360", "flow360._api", "pandas") @@ -117,3 +131,26 @@ def test_flow360_version_check_legacy_lazy_attribute_does_not_import_api_module( assert flow360.version_check.__name__ == "flow360.version_check" assert "flow360._api" not in sys.modules + + +def test_flow360_stub_reexports_match_lazy_api_exports(): + stub_source = Path(__file__).parents[1] / "flow360" / "__init__.pyi" + module_ast = ast.parse(stub_source.read_text(encoding="utf-8")) + + stub_api_reexports = set() + has_version_check_reexport = False + for node in module_ast.body: + if not isinstance(node, ast.ImportFrom): + continue + if node.module == "_api" and node.level == 1: + for imported_name in node.names: + assert imported_name.asname == imported_name.name + stub_api_reexports.add(imported_name.name) + if node.module is None and node.level == 1: + has_version_check_reexport = any( + imported_name.name == imported_name.asname == "version_check" + for imported_name in node.names + ) + + assert stub_api_reexports == set(_load_api_all()) + assert has_version_check_reexport From 7c428c6314c9406ef89756182b9d1bda9e92d296 Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Fri, 1 May 2026 10:01:29 +0200 Subject: [PATCH 6/9] Rename lazy exports module to public namespace --- flow360/__init__.py | 20 +- flow360/__init__.pyi | 358 +++++++++++----------- flow360/{_api.py => _public_namespace.py} | 0 tests/test_lazy_imports.py | 36 ++- 4 files changed, 214 insertions(+), 200 deletions(-) rename flow360/{_api.py => _public_namespace.py} (100%) diff --git a/flow360/__init__.py b/flow360/__init__.py index 9950c9067..61f8cd4c8 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -9,19 +9,19 @@ from pathlib import Path from types import ModuleType -_API_MODULE: ModuleType | None = None +_PUBLIC_NAMESPACE_MODULE: ModuleType | None = None -def _load_api_module() -> ModuleType: - global _API_MODULE # pylint: disable=global-statement - if _API_MODULE is None: - _API_MODULE = import_module("flow360._api") - return _API_MODULE +def _load_public_namespace_module() -> ModuleType: + global _PUBLIC_NAMESPACE_MODULE # pylint: disable=global-statement + if _PUBLIC_NAMESPACE_MODULE is None: + _PUBLIC_NAMESPACE_MODULE = import_module("flow360._public_namespace") + return _PUBLIC_NAMESPACE_MODULE def _load_exported_names() -> list[str]: - api_source = Path(__file__).with_name("_api.py").read_text(encoding="utf-8") - module_ast = ast.parse(api_source) + namespace_source = Path(__file__).with_name("_public_namespace.py").read_text(encoding="utf-8") + module_ast = ast.parse(namespace_source) for node in module_ast.body: if not isinstance(node, ast.Assign): continue @@ -41,9 +41,9 @@ def __getattr__(name: str): return module if name in __all__: - api_module = _load_api_module() + namespace_module = _load_public_namespace_module() try: - value = getattr(api_module, name) + value = getattr(namespace_module, name) except AttributeError as error: raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from error globals()[name] = value diff --git a/flow360/__init__.pyi b/flow360/__init__.pyi index aa09036ae..3d090b978 100644 --- a/flow360/__init__.pyi +++ b/flow360/__init__.pyi @@ -1,180 +1,192 @@ """Static exports for the lazy Flow360 package facade.""" from . import version_check as version_check -from ._api import Accounts as Accounts -from ._api import ActuatorDisk as ActuatorDisk -from ._api import AdaptiveCFL as AdaptiveCFL -from ._api import AeroAcousticOutput as AeroAcousticOutput -from ._api import AerospaceCondition as AerospaceCondition -from ._api import Air as Air -from ._api import AngleBasedRefinement as AngleBasedRefinement -from ._api import AngleExpression as AngleExpression -from ._api import AngularVelocity as AngularVelocity -from ._api import AspectRatioBasedRefinement as AspectRatioBasedRefinement -from ._api import AutomatedFarfield as AutomatedFarfield -from ._api import AxisymmetricBody as AxisymmetricBody -from ._api import AxisymmetricRefinement as AxisymmetricRefinement -from ._api import BETDisk as BETDisk -from ._api import BETDiskChord as BETDiskChord -from ._api import BETDiskSectionalPolar as BETDiskSectionalPolar -from ._api import BETDiskTwist as BETDiskTwist -from ._api import BodyGroupSelector as BodyGroupSelector -from ._api import BoundaryLayer as BoundaryLayer -from ._api import Box as Box -from ._api import C81File as C81File -from ._api import Case as Case -from ._api import CentralBelt as CentralBelt -from ._api import CGS_unit_system as CGS_unit_system -from ._api import CoordinateSystem as CoordinateSystem -from ._api import CustomVolume as CustomVolume -from ._api import CustomZones as CustomZones -from ._api import Cylinder as Cylinder -from ._api import DetachedEddySimulation as DetachedEddySimulation -from ._api import DFDCFile as DFDCFile -from ._api import EdgeSelector as EdgeSelector -from ._api import Env as Env -from ._api import Fluid as Fluid -from ._api import Folder as Folder -from ._api import ForceDistributionOutput as ForceDistributionOutput -from ._api import ForceOutput as ForceOutput -from ._api import ForcePerArea as ForcePerArea -from ._api import Freestream as Freestream -from ._api import FromUserDefinedDynamics as FromUserDefinedDynamics -from ._api import FrozenSpecies as FrozenSpecies -from ._api import FullyMovingFloor as FullyMovingFloor -from ._api import GenericReferenceCondition as GenericReferenceCondition -from ._api import Geometry as Geometry -from ._api import GeometryRefinement as GeometryRefinement -from ._api import Gravity as Gravity -from ._api import HeatEquationInitialCondition as HeatEquationInitialCondition -from ._api import HeatEquationSolver as HeatEquationSolver -from ._api import HeatFlux as HeatFlux -from ._api import HeightBasedRefinement as HeightBasedRefinement -from ._api import Inflow as Inflow -from ._api import Isosurface as Isosurface -from ._api import IsosurfaceOutput as IsosurfaceOutput -from ._api import KOmegaSST as KOmegaSST -from ._api import KOmegaSSTModelConstants as KOmegaSSTModelConstants -from ._api import KrylovLinearSolver as KrylovLinearSolver -from ._api import LinearSolver as LinearSolver -from ._api import LineSearch as LineSearch -from ._api import LiquidOperatingCondition as LiquidOperatingCondition -from ._api import Mach as Mach -from ._api import MassFlowRate as MassFlowRate -from ._api import MeshingDefaults as MeshingDefaults -from ._api import MeshingParams as MeshingParams -from ._api import MeshSliceOutput as MeshSliceOutput -from ._api import MirrorPlane as MirrorPlane -from ._api import ModularMeshingWorkflow as ModularMeshingWorkflow -from ._api import MovingStatistic as MovingStatistic -from ._api import NASA9Coefficients as NASA9Coefficients -from ._api import NASA9CoefficientSet as NASA9CoefficientSet -from ._api import NavierStokesInitialCondition as NavierStokesInitialCondition -from ._api import ( +from ._public_namespace import Accounts as Accounts +from ._public_namespace import ActuatorDisk as ActuatorDisk +from ._public_namespace import AdaptiveCFL as AdaptiveCFL +from ._public_namespace import AeroAcousticOutput as AeroAcousticOutput +from ._public_namespace import AerospaceCondition as AerospaceCondition +from ._public_namespace import Air as Air +from ._public_namespace import AngleBasedRefinement as AngleBasedRefinement +from ._public_namespace import AngleExpression as AngleExpression +from ._public_namespace import AngularVelocity as AngularVelocity +from ._public_namespace import AspectRatioBasedRefinement as AspectRatioBasedRefinement +from ._public_namespace import AutomatedFarfield as AutomatedFarfield +from ._public_namespace import AxisymmetricBody as AxisymmetricBody +from ._public_namespace import AxisymmetricRefinement as AxisymmetricRefinement +from ._public_namespace import BETDisk as BETDisk +from ._public_namespace import BETDiskChord as BETDiskChord +from ._public_namespace import BETDiskSectionalPolar as BETDiskSectionalPolar +from ._public_namespace import BETDiskTwist as BETDiskTwist +from ._public_namespace import BodyGroupSelector as BodyGroupSelector +from ._public_namespace import BoundaryLayer as BoundaryLayer +from ._public_namespace import Box as Box +from ._public_namespace import C81File as C81File +from ._public_namespace import Case as Case +from ._public_namespace import CentralBelt as CentralBelt +from ._public_namespace import CGS_unit_system as CGS_unit_system +from ._public_namespace import CoordinateSystem as CoordinateSystem +from ._public_namespace import CustomVolume as CustomVolume +from ._public_namespace import CustomZones as CustomZones +from ._public_namespace import Cylinder as Cylinder +from ._public_namespace import DetachedEddySimulation as DetachedEddySimulation +from ._public_namespace import DFDCFile as DFDCFile +from ._public_namespace import EdgeSelector as EdgeSelector +from ._public_namespace import Env as Env +from ._public_namespace import Fluid as Fluid +from ._public_namespace import Folder as Folder +from ._public_namespace import ForceDistributionOutput as ForceDistributionOutput +from ._public_namespace import ForceOutput as ForceOutput +from ._public_namespace import ForcePerArea as ForcePerArea +from ._public_namespace import Freestream as Freestream +from ._public_namespace import FromUserDefinedDynamics as FromUserDefinedDynamics +from ._public_namespace import FrozenSpecies as FrozenSpecies +from ._public_namespace import FullyMovingFloor as FullyMovingFloor +from ._public_namespace import GenericReferenceCondition as GenericReferenceCondition +from ._public_namespace import Geometry as Geometry +from ._public_namespace import GeometryRefinement as GeometryRefinement +from ._public_namespace import Gravity as Gravity +from ._public_namespace import ( + HeatEquationInitialCondition as HeatEquationInitialCondition, +) +from ._public_namespace import HeatEquationSolver as HeatEquationSolver +from ._public_namespace import HeatFlux as HeatFlux +from ._public_namespace import HeightBasedRefinement as HeightBasedRefinement +from ._public_namespace import Inflow as Inflow +from ._public_namespace import Isosurface as Isosurface +from ._public_namespace import IsosurfaceOutput as IsosurfaceOutput +from ._public_namespace import KOmegaSST as KOmegaSST +from ._public_namespace import KOmegaSSTModelConstants as KOmegaSSTModelConstants +from ._public_namespace import KrylovLinearSolver as KrylovLinearSolver +from ._public_namespace import LinearSolver as LinearSolver +from ._public_namespace import LineSearch as LineSearch +from ._public_namespace import LiquidOperatingCondition as LiquidOperatingCondition +from ._public_namespace import Mach as Mach +from ._public_namespace import MassFlowRate as MassFlowRate +from ._public_namespace import MeshingDefaults as MeshingDefaults +from ._public_namespace import MeshingParams as MeshingParams +from ._public_namespace import MeshSliceOutput as MeshSliceOutput +from ._public_namespace import MirrorPlane as MirrorPlane +from ._public_namespace import ModularMeshingWorkflow as ModularMeshingWorkflow +from ._public_namespace import MovingStatistic as MovingStatistic +from ._public_namespace import NASA9Coefficients as NASA9Coefficients +from ._public_namespace import NASA9CoefficientSet as NASA9CoefficientSet +from ._public_namespace import ( + NavierStokesInitialCondition as NavierStokesInitialCondition, +) +from ._public_namespace import ( NavierStokesModifiedRestartSolution as NavierStokesModifiedRestartSolution, ) -from ._api import NavierStokesSolver as NavierStokesSolver -from ._api import NoneSolver as NoneSolver -from ._api import Observer as Observer -from ._api import OctreeSpacing as OctreeSpacing -from ._api import Outflow as Outflow -from ._api import PassiveSpacing as PassiveSpacing -from ._api import Periodic as Periodic -from ._api import Point as Point -from ._api import PointArray as PointArray -from ._api import PointArray2D as PointArray2D -from ._api import PorousJump as PorousJump -from ._api import PorousMedium as PorousMedium -from ._api import Pressure as Pressure -from ._api import ProbeOutput as ProbeOutput -from ._api import Project as Project -from ._api import ProjectAnisoSpacing as ProjectAnisoSpacing -from ._api import RampCFL as RampCFL -from ._api import ReferenceGeometry as ReferenceGeometry -from ._api import RenderOutput as RenderOutput -from ._api import RenderOutputGroup as RenderOutputGroup -from ._api import Rotation as Rotation -from ._api import Rotational as Rotational -from ._api import RotationCylinder as RotationCylinder -from ._api import RotationSphere as RotationSphere -from ._api import RotationVolume as RotationVolume -from ._api import RunControl as RunControl -from ._api import SeedpointVolume as SeedpointVolume -from ._api import SI_unit_system as SI_unit_system -from ._api import SimulationParams as SimulationParams -from ._api import SlaterPorousBleed as SlaterPorousBleed -from ._api import Slice as Slice -from ._api import SliceOutput as SliceOutput -from ._api import SlipWall as SlipWall -from ._api import Solid as Solid -from ._api import SolidMaterial as SolidMaterial -from ._api import SpalartAllmaras as SpalartAllmaras -from ._api import SpalartAllmarasModelConstants as SpalartAllmarasModelConstants -from ._api import Sphere as Sphere -from ._api import StaticFloor as StaticFloor -from ._api import Steady as Steady -from ._api import StoppingCriterion as StoppingCriterion -from ._api import StreamlineOutput as StreamlineOutput -from ._api import StructuredBoxRefinement as StructuredBoxRefinement -from ._api import Supersonic as Supersonic -from ._api import SurfaceEdgeRefinement as SurfaceEdgeRefinement -from ._api import SurfaceIntegralOutput as SurfaceIntegralOutput -from ._api import SurfaceMesh as SurfaceMesh -from ._api import SurfaceOutput as SurfaceOutput -from ._api import SurfaceProbeOutput as SurfaceProbeOutput -from ._api import SurfaceRefinement as SurfaceRefinement -from ._api import SurfaceSliceOutput as SurfaceSliceOutput -from ._api import Sutherland as Sutherland -from ._api import SymmetryPlane as SymmetryPlane -from ._api import Temperature as Temperature -from ._api import ThermallyPerfectGas as ThermallyPerfectGas -from ._api import ThermalState as ThermalState -from ._api import ( +from ._public_namespace import NavierStokesSolver as NavierStokesSolver +from ._public_namespace import NoneSolver as NoneSolver +from ._public_namespace import Observer as Observer +from ._public_namespace import OctreeSpacing as OctreeSpacing +from ._public_namespace import Outflow as Outflow +from ._public_namespace import PassiveSpacing as PassiveSpacing +from ._public_namespace import Periodic as Periodic +from ._public_namespace import Point as Point +from ._public_namespace import PointArray as PointArray +from ._public_namespace import PointArray2D as PointArray2D +from ._public_namespace import PorousJump as PorousJump +from ._public_namespace import PorousMedium as PorousMedium +from ._public_namespace import Pressure as Pressure +from ._public_namespace import ProbeOutput as ProbeOutput +from ._public_namespace import Project as Project +from ._public_namespace import ProjectAnisoSpacing as ProjectAnisoSpacing +from ._public_namespace import RampCFL as RampCFL +from ._public_namespace import ReferenceGeometry as ReferenceGeometry +from ._public_namespace import RenderOutput as RenderOutput +from ._public_namespace import RenderOutputGroup as RenderOutputGroup +from ._public_namespace import Rotation as Rotation +from ._public_namespace import Rotational as Rotational +from ._public_namespace import RotationCylinder as RotationCylinder +from ._public_namespace import RotationSphere as RotationSphere +from ._public_namespace import RotationVolume as RotationVolume +from ._public_namespace import RunControl as RunControl +from ._public_namespace import SeedpointVolume as SeedpointVolume +from ._public_namespace import SI_unit_system as SI_unit_system +from ._public_namespace import SimulationParams as SimulationParams +from ._public_namespace import SlaterPorousBleed as SlaterPorousBleed +from ._public_namespace import Slice as Slice +from ._public_namespace import SliceOutput as SliceOutput +from ._public_namespace import SlipWall as SlipWall +from ._public_namespace import Solid as Solid +from ._public_namespace import SolidMaterial as SolidMaterial +from ._public_namespace import SpalartAllmaras as SpalartAllmaras +from ._public_namespace import ( + SpalartAllmarasModelConstants as SpalartAllmarasModelConstants, +) +from ._public_namespace import Sphere as Sphere +from ._public_namespace import StaticFloor as StaticFloor +from ._public_namespace import Steady as Steady +from ._public_namespace import StoppingCriterion as StoppingCriterion +from ._public_namespace import StreamlineOutput as StreamlineOutput +from ._public_namespace import StructuredBoxRefinement as StructuredBoxRefinement +from ._public_namespace import Supersonic as Supersonic +from ._public_namespace import SurfaceEdgeRefinement as SurfaceEdgeRefinement +from ._public_namespace import SurfaceIntegralOutput as SurfaceIntegralOutput +from ._public_namespace import SurfaceMesh as SurfaceMesh +from ._public_namespace import SurfaceOutput as SurfaceOutput +from ._public_namespace import SurfaceProbeOutput as SurfaceProbeOutput +from ._public_namespace import SurfaceRefinement as SurfaceRefinement +from ._public_namespace import SurfaceSliceOutput as SurfaceSliceOutput +from ._public_namespace import Sutherland as Sutherland +from ._public_namespace import SymmetryPlane as SymmetryPlane +from ._public_namespace import Temperature as Temperature +from ._public_namespace import ThermallyPerfectGas as ThermallyPerfectGas +from ._public_namespace import ThermalState as ThermalState +from ._public_namespace import ( TimeAverageForceDistributionOutput as TimeAverageForceDistributionOutput, ) -from ._api import TimeAverageIsosurfaceOutput as TimeAverageIsosurfaceOutput -from ._api import TimeAverageProbeOutput as TimeAverageProbeOutput -from ._api import TimeAverageSliceOutput as TimeAverageSliceOutput -from ._api import TimeAverageStreamlineOutput as TimeAverageStreamlineOutput -from ._api import TimeAverageSurfaceOutput as TimeAverageSurfaceOutput -from ._api import TimeAverageSurfaceProbeOutput as TimeAverageSurfaceProbeOutput -from ._api import TimeAverageVolumeOutput as TimeAverageVolumeOutput -from ._api import TotalPressure as TotalPressure -from ._api import TransitionModelSolver as TransitionModelSolver -from ._api import Translational as Translational -from ._api import TurbulenceModelControls as TurbulenceModelControls -from ._api import TurbulenceQuantities as TurbulenceQuantities -from ._api import UniformRefinement as UniformRefinement -from ._api import Unsteady as Unsteady -from ._api import UserDefinedDynamic as UserDefinedDynamic -from ._api import UserDefinedFarfield as UserDefinedFarfield -from ._api import UserDefinedField as UserDefinedField -from ._api import UserVariable as UserVariable -from ._api import VolumeMesh as VolumeMesh -from ._api import VolumeMeshingDefaults as VolumeMeshingDefaults -from ._api import VolumeMeshingParams as VolumeMeshingParams -from ._api import VolumeOutput as VolumeOutput -from ._api import VolumeSelector as VolumeSelector -from ._api import Wall as Wall -from ._api import WallFunction as WallFunction -from ._api import WallRotation as WallRotation -from ._api import Water as Water -from ._api import WheelBelts as WheelBelts -from ._api import WindTunnelFarfield as WindTunnelFarfield -from ._api import XFOILFile as XFOILFile -from ._api import XROTORFile as XROTORFile -from ._api import configure as configure -from ._api import create_draft as create_draft -from ._api import get_user_variable as get_user_variable -from ._api import imperial_unit_system as imperial_unit_system -from ._api import math as math -from ._api import migration as migration -from ._api import remove_user_variable as remove_user_variable -from ._api import render_config as render_config -from ._api import report as report -from ._api import services as services -from ._api import show_available_examples as show_available_examples -from ._api import show_user_variables as show_user_variables -from ._api import snappy as snappy -from ._api import solution as solution -from ._api import u as u +from ._public_namespace import ( + TimeAverageIsosurfaceOutput as TimeAverageIsosurfaceOutput, +) +from ._public_namespace import TimeAverageProbeOutput as TimeAverageProbeOutput +from ._public_namespace import TimeAverageSliceOutput as TimeAverageSliceOutput +from ._public_namespace import ( + TimeAverageStreamlineOutput as TimeAverageStreamlineOutput, +) +from ._public_namespace import TimeAverageSurfaceOutput as TimeAverageSurfaceOutput +from ._public_namespace import ( + TimeAverageSurfaceProbeOutput as TimeAverageSurfaceProbeOutput, +) +from ._public_namespace import TimeAverageVolumeOutput as TimeAverageVolumeOutput +from ._public_namespace import TotalPressure as TotalPressure +from ._public_namespace import TransitionModelSolver as TransitionModelSolver +from ._public_namespace import Translational as Translational +from ._public_namespace import TurbulenceModelControls as TurbulenceModelControls +from ._public_namespace import TurbulenceQuantities as TurbulenceQuantities +from ._public_namespace import UniformRefinement as UniformRefinement +from ._public_namespace import Unsteady as Unsteady +from ._public_namespace import UserDefinedDynamic as UserDefinedDynamic +from ._public_namespace import UserDefinedFarfield as UserDefinedFarfield +from ._public_namespace import UserDefinedField as UserDefinedField +from ._public_namespace import UserVariable as UserVariable +from ._public_namespace import VolumeMesh as VolumeMesh +from ._public_namespace import VolumeMeshingDefaults as VolumeMeshingDefaults +from ._public_namespace import VolumeMeshingParams as VolumeMeshingParams +from ._public_namespace import VolumeOutput as VolumeOutput +from ._public_namespace import VolumeSelector as VolumeSelector +from ._public_namespace import Wall as Wall +from ._public_namespace import WallFunction as WallFunction +from ._public_namespace import WallRotation as WallRotation +from ._public_namespace import Water as Water +from ._public_namespace import WheelBelts as WheelBelts +from ._public_namespace import WindTunnelFarfield as WindTunnelFarfield +from ._public_namespace import XFOILFile as XFOILFile +from ._public_namespace import XROTORFile as XROTORFile +from ._public_namespace import configure as configure +from ._public_namespace import create_draft as create_draft +from ._public_namespace import get_user_variable as get_user_variable +from ._public_namespace import imperial_unit_system as imperial_unit_system +from ._public_namespace import math as math +from ._public_namespace import migration as migration +from ._public_namespace import remove_user_variable as remove_user_variable +from ._public_namespace import render_config as render_config +from ._public_namespace import report as report +from ._public_namespace import services as services +from ._public_namespace import show_available_examples as show_available_examples +from ._public_namespace import show_user_variables as show_user_variables +from ._public_namespace import snappy as snappy +from ._public_namespace import solution as solution +from ._public_namespace import u as u diff --git a/flow360/_api.py b/flow360/_public_namespace.py similarity index 100% rename from flow360/_api.py rename to flow360/_public_namespace.py diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index cc0920d78..eb57935b2 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -11,26 +11,26 @@ def _unload_modules(monkeypatch, *module_names): monkeypatch.delitem(sys.modules, module_name, raising=False) -def _load_api_all(): - api_source = Path(__file__).parents[1] / "flow360" / "_api.py" - module_ast = ast.parse(api_source.read_text(encoding="utf-8")) +def _load_public_namespace_all(): + namespace_source = Path(__file__).parents[1] / "flow360" / "_public_namespace.py" + module_ast = ast.parse(namespace_source.read_text(encoding="utf-8")) for node in module_ast.body: if not isinstance(node, ast.Assign): continue for target in node.targets: if isinstance(target, ast.Name) and target.id == "__all__": return ast.literal_eval(node.value) - raise AssertionError("flow360._api must define __all__") + raise AssertionError("flow360._public_namespace must define __all__") def test_import_flow360_does_not_eagerly_import_heavy_dependencies(monkeypatch): monkeypatch.delenv("FLOW360_SUPPRESS_BETA_WARNING", raising=False) - _unload_modules(monkeypatch, "flow360", "flow360._api", "pandas") + _unload_modules(monkeypatch, "flow360", "flow360._public_namespace", "pandas") import flow360 # pylint: disable=import-outside-toplevel,import-error assert "pandas" not in sys.modules - assert "flow360._api" not in sys.modules + assert "flow360._public_namespace" not in sys.modules assert "Env" in flow360.__all__ @@ -105,12 +105,12 @@ def test_sdk_configure_helper_does_not_import_cli_modules(monkeypatch, tmp_path) assert "flow360.cli.api_set_func" not in sys.modules -def test_flow360_configure_is_exposed_without_importing_api_module(monkeypatch): +def test_flow360_configure_is_exposed_without_importing_public_namespace_module(monkeypatch): monkeypatch.delenv("FLOW360_SUPPRESS_BETA_WARNING", raising=False) _unload_modules( monkeypatch, "flow360", - "flow360._api", + "flow360._public_namespace", "flow360.cli", "flow360.cli.app", "flow360.cli.api_set_func", @@ -119,38 +119,40 @@ def test_flow360_configure_is_exposed_without_importing_api_module(monkeypatch): import flow360 # pylint: disable=import-outside-toplevel,import-error assert "configure" in flow360.__all__ - assert "flow360._api" not in sys.modules + assert "flow360._public_namespace" not in sys.modules assert "flow360.cli.app" not in sys.modules assert "flow360.cli.api_set_func" not in sys.modules -def test_flow360_version_check_legacy_lazy_attribute_does_not_import_api_module(monkeypatch): - _unload_modules(monkeypatch, "flow360", "flow360._api", "flow360.version_check") +def test_flow360_version_check_legacy_lazy_attribute_does_not_import_public_namespace_module( + monkeypatch, +): + _unload_modules(monkeypatch, "flow360", "flow360._public_namespace", "flow360.version_check") import flow360 # pylint: disable=import-outside-toplevel,import-error assert flow360.version_check.__name__ == "flow360.version_check" - assert "flow360._api" not in sys.modules + assert "flow360._public_namespace" not in sys.modules -def test_flow360_stub_reexports_match_lazy_api_exports(): +def test_flow360_stub_reexports_match_lazy_public_namespace_exports(): stub_source = Path(__file__).parents[1] / "flow360" / "__init__.pyi" module_ast = ast.parse(stub_source.read_text(encoding="utf-8")) - stub_api_reexports = set() + stub_namespace_reexports = set() has_version_check_reexport = False for node in module_ast.body: if not isinstance(node, ast.ImportFrom): continue - if node.module == "_api" and node.level == 1: + if node.module == "_public_namespace" and node.level == 1: for imported_name in node.names: assert imported_name.asname == imported_name.name - stub_api_reexports.add(imported_name.name) + stub_namespace_reexports.add(imported_name.name) if node.module is None and node.level == 1: has_version_check_reexport = any( imported_name.name == imported_name.asname == "version_check" for imported_name in node.names ) - assert stub_api_reexports == set(_load_api_all()) + assert stub_namespace_reexports == set(_load_public_namespace_all()) assert has_version_check_reexport From 71251f28ee4c307d7537fdeb24f119279a8cd131 Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Fri, 1 May 2026 10:53:57 +0200 Subject: [PATCH 7/9] Keep config refactor out of lazy import PR --- flow360/_public_namespace.py | 11 +++++- flow360/cli/api_set_func.py | 28 +++++++++++++-- flow360/user_config.py | 67 +++--------------------------------- tests/test_lazy_imports.py | 24 ++++++------- 4 files changed, 53 insertions(+), 77 deletions(-) diff --git a/flow360/_public_namespace.py b/flow360/_public_namespace.py index 9d361957c..92dae827e 100644 --- a/flow360/_public_namespace.py +++ b/flow360/_public_namespace.py @@ -213,9 +213,18 @@ from flow360.component.volume_mesh import VolumeMeshV2 as VolumeMesh from flow360.environment import Env from flow360.plugins import report -from flow360.user_config import configure_apikey as configure from flow360.version_utils import warn_if_prerelease_version as _warn_prerelease + +def configure(apikey: str, environment: str = None, profile: str = "default") -> None: + """Function interface for configuring the API key for flow360.""" + from flow360.cli.api_set_func import ( + configure_caller, # pylint: disable=import-outside-toplevel + ) + + configure_caller(apikey=apikey, environment=environment, profile=profile) + + __all__ = [ "GeometryRefinement", "Env", diff --git a/flow360/cli/api_set_func.py b/flow360/cli/api_set_func.py index 6b6b32378..b36345a16 100644 --- a/flow360/cli/api_set_func.py +++ b/flow360/cli/api_set_func.py @@ -1,6 +1,10 @@ """Helper function to set up the API key for the user.""" -from flow360.user_config import configure_apikey +from click.testing import CliRunner + +import flow360.user_config as user_config # pylint: disable=consider-using-from-import +from flow360.cli.app import configure +from flow360.log import log def configure_caller(apikey: str, environment: str = None, profile: str = "default") -> None: @@ -15,4 +19,24 @@ def configure_caller(apikey: str, environment: str = None, profile: str = "defau Returns: None """ - configure_apikey(apikey=apikey, environment=environment, profile=profile) + runner = CliRunner() + + # Construct CLI arguments as a list + args = ["--apikey", apikey, "--profile", profile] + + if environment: + if environment.lower() in ("dev", "uat"): + args += ["--" + environment.lower()] + elif environment.lower() == "prod": + args += [] + else: + args += ["--env", environment] + + # Invoke the `configure` command + result = runner.invoke(configure, args) + + if result.exit_code != 0: + log.error(result.output if result.output else str(result.exception)) + else: + log.info("Configuration successful.") + user_config.UserConfig = user_config.BasicUserConfig() # Reload diff --git a/flow360/user_config.py b/flow360/user_config.py index 35be80c6f..134a06f51 100644 --- a/flow360/user_config.py +++ b/flow360/user_config.py @@ -17,35 +17,6 @@ CONFIG_FILE_MODE = 0o600 -def _merge_overwrite(old: dict, new: dict): - """Deep-merge dictionaries while overwriting conflicts from `new`.""" - - for key, value in new.items(): - if key in old and isinstance(old[key], dict) and isinstance(value, dict): - _merge_overwrite(old[key], value) - else: - old[key] = value - return old - - -def _normalize_storage_environment_name(environment: Optional[str]) -> Optional[str]: - """Normalize environment names used for config storage.""" - - if environment is None: - return None - - normalized = environment.strip() - if not normalized: - return None - - lowered = normalized.lower() - if lowered == prod.name: - return None - if lowered in ("dev", "uat"): - return lowered - return normalized - - def _ensure_permissions(path: str, mode: int): """Best-effort permission hardening for local config paths.""" try: @@ -84,34 +55,20 @@ def store_apikey( ): """Store an API key using the same config layout consumed by UserConfig.""" config = read_user_config() - environment_name = _normalize_storage_environment_name(environment_name) if environment_name in (None, "", prod.name): entry = {profile: {"apikey": apikey}} else: entry = {profile: {environment_name: {"apikey": apikey}}} - _merge_overwrite(config, entry) + # Avoid importing CLI modules at import time because the wider package has lazy-import paths. + from flow360.cli import dict_utils # pylint: disable=import-outside-toplevel + + dict_utils.merge_overwrite(config, entry) write_user_config(config) return config -def configure_apikey( - apikey: str, - environment: Optional[str] = None, - profile: str = DEFAULT_PROFILE, -) -> None: - """SDK-facing helper for storing an API key without going through the CLI app.""" - - store_apikey( - apikey, - profile=profile, - environment_name=environment, - ) - reload_user_config() - log.info("Configuration successful.") - - def delete_apikey(profile: str = DEFAULT_PROFILE, environment_name: Optional[str] = None): """Delete a stored API key for the selected profile/environment if present.""" config = read_user_config() @@ -205,7 +162,7 @@ def apikey(self, env): # If other environment is used, check if the key exists key = key.get(env.name, None) if key is None: - log.debug(f"No api key configured for environment '{env.name}'.") + log.warning(f"Cannot find api key associated with environment '{env.name}'.") return None if key is None else key.get("apikey", "") def suppress_submit_warning(self): @@ -252,18 +209,4 @@ def enable_validation(self): self._do_validation = True -def reload_user_config(): - """Reload the shared user-config object in place when possible.""" - - global UserConfig # pylint: disable=global-statement - - current_user_config = globals().get("UserConfig") - if isinstance(current_user_config, BasicUserConfig): - BasicUserConfig.__init__(current_user_config) - return current_user_config - - UserConfig = BasicUserConfig() - return UserConfig - - UserConfig = BasicUserConfig() diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index eb57935b2..d60238863 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -2,7 +2,6 @@ import sys from pathlib import Path -import toml from click.testing import CliRunner @@ -87,20 +86,21 @@ def test_flow360_root_help_does_not_eagerly_import_sdk_command_modules(monkeypat assert "flow360.cloud.flow360_requests" not in sys.modules -def test_sdk_configure_helper_does_not_import_cli_modules(monkeypatch, tmp_path): +def test_public_namespace_configure_does_not_eagerly_import_cli_modules(monkeypatch): monkeypatch.delenv("FLOW360_SUPPRESS_BETA_WARNING", raising=False) - config_path = tmp_path / "config.toml" - - _unload_modules(monkeypatch, "flow360.cli", "flow360.cli.app", "flow360.cli.api_set_func") - - import flow360.user_config as user_config # pylint: disable=import-outside-toplevel,import-error - - monkeypatch.setattr(user_config, "config_file", str(config_path)) + _unload_modules( + monkeypatch, + "flow360._public_namespace", + "flow360.cli", + "flow360.cli.app", + "flow360.cli.api_set_func", + ) - user_config.configure_apikey("test-key", environment="dev", profile="default") + from flow360 import ( + _public_namespace, # pylint: disable=import-outside-toplevel,import-error + ) - config = toml.loads(config_path.read_text()) - assert config["default"]["dev"]["apikey"] == "test-key" + assert callable(_public_namespace.configure) assert "flow360.cli.app" not in sys.modules assert "flow360.cli.api_set_func" not in sys.modules From 95e2ba6161fa865e8bac37f3fc1e248c0b24f36c Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Fri, 1 May 2026 11:00:24 +0200 Subject: [PATCH 8/9] Fix lazy configure pylint suppression --- flow360/_public_namespace.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flow360/_public_namespace.py b/flow360/_public_namespace.py index 92dae827e..29f5e078c 100644 --- a/flow360/_public_namespace.py +++ b/flow360/_public_namespace.py @@ -218,9 +218,9 @@ def configure(apikey: str, environment: str = None, profile: str = "default") -> None: """Function interface for configuring the API key for flow360.""" - from flow360.cli.api_set_func import ( - configure_caller, # pylint: disable=import-outside-toplevel - ) + # Keep the legacy configure path lazy so `import flow360` does not load Click. + # pylint: disable=import-outside-toplevel + from flow360.cli.api_set_func import configure_caller configure_caller(apikey=apikey, environment=environment, profile=profile) From f33458c9a7c496ff569a4e566ae8d1091bf0cee3 Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Fri, 1 May 2026 11:44:03 +0200 Subject: [PATCH 9/9] Fail fast when lazy exports are missing --- flow360/__init__.py | 32 +++++++++++++++++++++++++------- tests/test_lazy_imports.py | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/flow360/__init__.py b/flow360/__init__.py index 61f8cd4c8..ea1dee1b7 100644 --- a/flow360/__init__.py +++ b/flow360/__init__.py @@ -19,16 +19,34 @@ def _load_public_namespace_module() -> ModuleType: return _PUBLIC_NAMESPACE_MODULE +def _is_all_target(target: ast.expr) -> bool: + return isinstance(target, ast.Name) and target.id == "__all__" + + +def _literal_exported_names(value: ast.expr | None) -> list[str]: + if value is None: + raise RuntimeError("flow360._public_namespace must assign __all__") + names = ast.literal_eval(value) + if not isinstance(names, list) or not all(isinstance(name, str) for name in names): + raise RuntimeError("flow360._public_namespace __all__ must be a list of strings") + return names + + +def _extract_exported_names(module_ast: ast.Module) -> list[str]: + for node in module_ast.body: + if isinstance(node, ast.Assign): + for target in node.targets: + if _is_all_target(target): + return _literal_exported_names(node.value) + if isinstance(node, ast.AnnAssign) and _is_all_target(node.target): + return _literal_exported_names(node.value) + raise RuntimeError("flow360._public_namespace must define __all__") + + def _load_exported_names() -> list[str]: namespace_source = Path(__file__).with_name("_public_namespace.py").read_text(encoding="utf-8") module_ast = ast.parse(namespace_source) - for node in module_ast.body: - if not isinstance(node, ast.Assign): - continue - for target in node.targets: - if isinstance(target, ast.Name) and target.id == "__all__": - return ast.literal_eval(node.value) - return [] + return _extract_exported_names(module_ast) __all__ = _load_exported_names() diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index d60238863..1ca673929 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -2,6 +2,7 @@ import sys from pathlib import Path +import pytest from click.testing import CliRunner @@ -14,11 +15,16 @@ def _load_public_namespace_all(): namespace_source = Path(__file__).parents[1] / "flow360" / "_public_namespace.py" module_ast = ast.parse(namespace_source.read_text(encoding="utf-8")) for node in module_ast.body: - if not isinstance(node, ast.Assign): - continue - for target in node.targets: - if isinstance(target, ast.Name) and target.id == "__all__": - return ast.literal_eval(node.value) + if isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name) and target.id == "__all__": + return ast.literal_eval(node.value) + if ( + isinstance(node, ast.AnnAssign) + and isinstance(node.target, ast.Name) + and node.target.id == "__all__" + ): + return ast.literal_eval(node.value) raise AssertionError("flow360._public_namespace must define __all__") @@ -124,6 +130,26 @@ def test_flow360_configure_is_exposed_without_importing_public_namespace_module( assert "flow360.cli.api_set_func" not in sys.modules +def test_exported_name_loader_fails_when_public_namespace_has_no_all(): + import flow360 # pylint: disable=import-outside-toplevel,import-error + + module_ast = ast.parse("PUBLIC_NAME = 'Env'") + + with pytest.raises(RuntimeError, match="must define __all__"): + flow360._extract_exported_names(module_ast) # pylint: disable=protected-access + + +def test_exported_name_loader_supports_annotated_all_assignment(): + import flow360 # pylint: disable=import-outside-toplevel,import-error + + module_ast = ast.parse("__all__: list[str] = ['Env', 'Project']") + + assert flow360._extract_exported_names(module_ast) == [ # pylint: disable=protected-access + "Env", + "Project", + ] + + def test_flow360_version_check_legacy_lazy_attribute_does_not_import_public_namespace_module( monkeypatch, ):