diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 22edd4d6b..b05774019 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,10 +14,7 @@ name: Docs - ".github/workflows/docs.yml" workflow_dispatch: -permissions: - contents: read - pages: write - id-token: write +permissions: {} concurrency: group: pages @@ -28,6 +25,8 @@ jobs: name: docs-quality runs-on: ubuntu-latest timeout-minutes: 30 + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -113,6 +112,8 @@ jobs: needs: docs-quality runs-on: ubuntu-latest timeout-minutes: 60 + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -158,6 +159,9 @@ jobs: if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + pages: write + id-token: write environment: name: ${{ 'github-pages' }} url: ${{ steps.deployment.outputs.page_url }} diff --git a/.markdownlint.json b/.markdownlint.json index b6c7a730e..d65a70ff4 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,9 +1,37 @@ { - "default": true, - "MD013": false, - "MD024": false, - "MD029": false, + "MD001": false, + "MD013": { + "code_block_line_length": 500, + "heading_line_length": 200, + "line_length": 500, + "tables": false + }, + "MD022": false, + "MD023": false, + "MD024": { + "siblings_only": true + }, + "MD025": false, + "MD031": false, + "MD033": { + "allowed_elements": [ + "br", + "details", + "summary", + "kbd" + ] + }, "MD036": false, "MD040": false, - "MD051": false + "MD041": false, + "MD042": false, + "MD046": false, + "MD050": { + "style": "consistent" + }, + "MD051": false, + "MD057": false, + "MD060": false, + "MD076": false, + "default": true } diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 000000000..8cd6607ab --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,24 @@ +.omo/** +.beads/** +.tmp/** +.claude/** +.vscode/** +.codegraph/** +.code-review-graph/** +.serena/** +.snapshots/** +.benchmarks/** +.hypothesis/** +.mypy_cache/** +.pytest_cache/** +.ruff_cache/** +.venv/** +__pycache__/** +node_modules/** +.reports/** +dist/** +build/** +htmlcov/** +legado/** +.github/** +site/** diff --git a/Makefile b/Makefile index 857aa57b8..99965f6d3 100644 --- a/Makefile +++ b/Makefile @@ -48,13 +48,13 @@ BRANCH ?= PYTEST_ARGS ?= PYTEST_DIAG_ARGS ?= -rA --durations=0 --tb=long --showlocals PYTEST_REPORT_ARGS ?= -ra --durations=25 --durations-min=0.001 --tb=short -PYTEST_PROCESS_TIMEOUT_SECONDS ?= 180 +PYTEST_PROCESS_TIMEOUT_SECONDS ?= 360 # mro-99ae: the pytest process inherits a hard wall-clock boundary, mirroring # MYPY_BOUNDED, so a hung run is terminated even if the typed runner stalls. PYTEST_BOUNDED = timeout --signal=TERM --kill-after=5s "$(PYTEST_PROCESS_TIMEOUT_SECONDS)s" PYTEST_REPORTS_DIR ?= .reports/tests override PYTEST_CASE_TIMEOUT_SECONDS := 10 -override PYTEST_RUN_TIMEOUT_SECONDS := 120 +override PYTEST_RUN_TIMEOUT_SECONDS := 300 override PYTEST_TERMINATION_GRACE_SECONDS := 2 override PYTEST_TIMEOUT_EXIT_CODE := 124 override PYTEST_ENFORCEMENT_PLUGIN := flext_tests_enforcement @@ -66,6 +66,16 @@ override PYTEST_PARALLEL_DISTRIBUTION := worksteal override PYTEST_PROFILE_SORT := cumulative override PYTEST_PROFILE_LIMIT := 50 override PROCESS_TIMEOUT_COMMAND := timeout +# CI ternary wall-clock budget per verb/what/project: CI=Y owns the fast +# gates (60s each); CI=N owns the slow whole-program analyses +# (300s each); an unset token runs unbounded. +ifeq ($(strip $(CI)),Y) +VERB_BOUNDED := timeout --signal=TERM --kill-after=5s 60s +else ifeq ($(strip $(CI)),N) +VERB_BOUNDED := timeout --signal=TERM --kill-after=5s 300s +else +VERB_BOUNDED := +endif override export FLEXT_PYTEST_ARGS_RAW := $(value PYTEST_ARGS) override export FLEXT_PYTEST_FILE_RAW := $(value FILE) override export FLEXT_PYTEST_FILES_RAW := $(value FILES) @@ -91,18 +101,6 @@ MAKEFILE_ROOT := $(patsubst %/,%,$(dir $(SELF_MAKEFILE))) PROJECT_ROOT := $(MAKEFILE_ROOT) override export FLEXT_PYTEST_TARGET_RAW := tests WORKSPACE ?= $(PROJECT_ROOT) -# A workspace lane is always registered at the workspace root. Other verbs may -# select a member through PROJECT, but `make work` keeps WORKSPACE at the root -# so one Git worktree owns the complete project matrix. -ifneq ($(filter work,$(MAKECMDGOALS)),work) -ifeq ($(filter command line override,$(origin WORKSPACE)),) -ifneq ($(strip $(PROJECT)),) -ifneq ($(filter $(PROJECT),$(WORKSPACE_MEMBERS)),) -override WORKSPACE := $(PROJECT_ROOT)/$(PROJECT) -endif -endif -endif -endif # === SECTION: WORKSPACE_ROOT isolation (managed) === # Source: computed (rule: derive from current checkout unless caller overrides) # Rule: WORKSPACE_ROOT is always derived from the current checkout unless the @@ -115,6 +113,18 @@ ifeq ($(filter command line override,$(origin WORKSPACE_ROOT)),) WORKSPACE_ROOT := $(shell cd "$(MAKEFILE_ROOT)" && root=$$(git rev-parse --show-superproject-working-tree 2>/dev/null); if [ -n "$$root" ]; then printf '%s\n' "$$root"; else git rev-parse --show-toplevel 2>/dev/null || printf '%s\n' "$(MAKEFILE_ROOT)"; fi) endif # End SECTION: WORKSPACE_ROOT isolation +# A workspace lane is always registered at the workspace root. Other verbs may +# select a member through PROJECT, but `make work` keeps WORKSPACE at the root +# so one Git worktree owns the complete project matrix. +ifneq ($(filter work,$(MAKECMDGOALS)),work) +ifeq ($(filter command line override,$(origin WORKSPACE)),) +ifneq ($(strip $(PROJECT)),) +ifneq ($(filter $(PROJECT),$(WORKSPACE_MEMBERS)),) +override WORKSPACE := $(WORKSPACE_ROOT)/$(PROJECT) +endif +endif +endif +endif # === SECTION: verb dispatch (managed) === # Source: config:make.verbs[*].whats, config:make.check_gates_allowed, @@ -139,7 +149,7 @@ _ALLOWED_WHATS_gen := check all apply $(shell sed -n 's/^_custom_gen_\([a-z0-9_- _ALLOWED_WHATS_work := start status land finish $(shell sed -n 's/^_custom_work_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') _ALLOWED_WHATS_mod := check all apply $(shell sed -n 's/^_custom_mod_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') -CHECK_GATES_ALLOWED := lint format pyrefly mypy pyright security markdown smells +CHECK_GATES_ALLOWED := lint pyrefly mypy pyright security markdown smells CHECK_GATES_DEFAULT := lint pyrefly mypy pyright security markdown smells # End SECTION: verb dispatch @@ -400,9 +410,9 @@ define _dispatch if [ "$$rc" -ne 2 ]; then $(SELF_MAKE) "$$hook" || exit $$?; fi; \ done; \ if [ "$$custom_rc" -ne 2 ]; then \ - $(SELF_MAKE) "$$custom" || exit $$?; \ + $(VERB_BOUNDED) $(SELF_MAKE) "$$custom" || exit $$?; \ else \ - $(SELF_MAKE) "$$builtin" || exit $$?; \ + $(VERB_BOUNDED) $(SELF_MAKE) "$$builtin" || exit $$?; \ fi; \ for hook in "post-$(1)-$$what" "post-$(1)"; do \ $(SELF_MAKE) -q "$$hook" >/dev/null 2>&1; rc=$$?; \ @@ -846,7 +856,7 @@ _builtin_deps_upgrade: _builtin_require_environment set --; \ for project in $$selected; do set -- "$$@" --projects "$$project"; done; \ $(PROJECT_FLEXT_INFRA) deps modernize --workspace "$(PROJECT_ROOT)" \ - --apply --rewrite-constraints --skip-check "$$@" + --apply $(if $(strip $(DEPENDENCY)),,--rewrite-constraints) --skip-check "$$@" $(call _run_for_selected_projects,) @@ -857,15 +867,19 @@ _builtin_build_artifacts: # by `make fix APPLY=Y` and formatting by `make fmt APPLY=Y`, both run BEFORE # check. APPLY here made the same tools run twice with conflicting intents, # so it is rejected instead of silently honoured; FIX=1 became the `fix` verb. -# CI=Y runs make.ci.check_gates (RULING 2: rules not skip-list). +# CI=Y runs make.ci.check_gates and CI=N runs its strict +# complement, make.ci.local_check_gates (RULING 2: rules not skip-list). _builtin_check_all: _builtin_require_environment @set -eu; \ gates="$(strip $(CHECK_GATES))"; \ if [ -z "$$gates" ]; then gates="$$(printf '%s' '$(CHECK_GATES_DEFAULT)' | tr ' ' ',')"; fi; \ gates="$$(printf '%s' "$$gates" | tr -d '[:space:]')"; \ if [ "$(strip $(CI))" = "Y" ]; then \ - gates="mypy,pyright,security,markdown,smells"; \ - printf 'INFO: CI=Y runs check gates: mypy pyright security markdown smells\n'; \ + gates="lint,pyright,security,markdown,smells"; \ + printf 'INFO: CI=Y runs check gates: lint pyright security markdown smells\n'; \ + elif [ "$(strip $(CI))" = "N" ]; then \ + gates="pyrefly,mypy"; \ + printf 'INFO: CI=N runs check gates: pyrefly mypy\n'; \ fi; \ for gate in $$(printf '%s' "$$gates" | tr ',' ' '); do \ case " $(CHECK_GATES_ALLOWED) " in *" $$gate "*) ;; \ @@ -931,7 +945,7 @@ _builtin_fix_check: _builtin_require_environment _builtin_fix_all: _builtin_require_environment $(call _require_apply) @$(PROJECT_FLEXT_INFRA) check run --workspace "$(PROJECT_ROOT)" --projects . --fix \ - --gates format,markdown,smells + --gates markdown,smells _builtin_fix_apply: _builtin_fix_all @@ -991,19 +1005,16 @@ _builtin_release_rel: _builtin_require_environment $$push_flag \ $$projects_args -# Generation preserves the caller's scope. Conform owns analyzer roots in the -# rendered tooling context; dependency modernization owns only dependency -# settings that conform does not render. +# Generation has one owner. Conform preserves the caller's scope and applies +# the complete dependency/tooling projection before it verifies its fixed point. +# Dependency upgrades remain a separate explicit verb because they rewrite lock +# floors; gen must never run a second pyproject writer over conform's result. _builtin_gen_check: _builtin_require_environment @$(PROJECT_FLEXT_INFRA) codegen conform --root "$(PROJECT_ROOT)" --scope "$(CODEGEN_SCOPE)" --mode check - @$(PROJECT_FLEXT_INFRA) codegen init --workspace "$(PROJECT_ROOT)" --check - @$(PROJECT_FLEXT_INFRA) deps modernize --workspace "$(PROJECT_ROOT)" --check _builtin_gen_all: _builtin_require_environment $(call _require_apply) @$(PROJECT_FLEXT_INFRA) codegen conform --root "$(PROJECT_ROOT)" --scope "$(CODEGEN_SCOPE)" --mode apply - @$(PROJECT_FLEXT_INFRA) codegen init --workspace "$(PROJECT_ROOT)" --apply - @$(PROJECT_FLEXT_INFRA) deps modernize --workspace "$(PROJECT_ROOT)" --apply _builtin_gen_apply: _builtin_gen_all diff --git a/docs/architecture.md b/docs/architecture.md index abffcc294..f336a71fd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -28,7 +28,7 @@ src/flext_cli/ ├── cli.py # Única fronteira com Typer/Click ├── cli_params.py # Parâmetros reutilizáveis para comandos Typer/Click ├── commands.py # Registro e resolução de comandos estruturais -├── settings.py # Singleton de configuração validada +├── _settings.py # Singleton de configuração validada ├── constants.py # Constantes e mensagens compartilhadas ├── debug.py # Utilidades de depuração ├── file_tools.py # I/O de arquivos (texto, JSON, YAML, CSV, zip) @@ -52,7 +52,7 @@ src/flext_cli/ 1. **Registro de comandos**: modelos em `commands.py` são validados em `FlextCliCore.register_command` antes de serem armazenados. 1. **Execução**: `FlextCliCore.execute_command` resolve o comando registrado; `FlextCliCmd` fornece operações utilitárias ligadas à configuração persistida. 1. **Entrada/Saída**: `prompts.py` coleta entrada; `output.py`, `formatters.py` e `tables.py` geram saídas em Rich/ASCII/JSON/YAML/CSV sem expor o Rich diretamente. -1. **Configuração**: `settings.py` gerencia configuração imutável; sessões são armazenadas em `core`. +1. **Configuração**: `_settings.py` gerencia configuração imutável; sessões são armazenadas em `core`. ## Integração com flext-core diff --git a/docs/development.md b/docs/development.md index 877d4e012..56eb84d15 100644 --- a/docs/development.md +++ b/docs/development.md @@ -206,7 +206,7 @@ src/flext_cli/ │ └── Data Models (value objects) ├── models.py # All Pydantic models - └── settings.py # FlextCliSettings + └── _settings.py # FlextCliSettings ``` ### Direct Access Pattern diff --git a/pyproject.toml b/pyproject.toml index 20fbe77ab..52463875d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,6 +140,7 @@ namespace_packages = true no_implicit_reexport = true plugins = ["pydantic.mypy"] python_version = "3.13" +show_traceback = true strict_equality = true warn_redundant_casts = true warn_return_any = true @@ -399,6 +400,7 @@ filterwarnings = [ "error", "module::flext_core._constants.enforcement.FlextMroViolation", ] +flext_slow_timeout_seconds = "60" markers = [ "docker: tests requiring Docker", "e2e: end-to-end integration tests", @@ -513,9 +515,20 @@ known-first-party = ["flext_cli", "flext_core", "flext_infra", "flext_tests"] split-on-trailing-comma = false [tool.ruff.lint.per-file-ignores] -"**/.vulture_whitelist.py" = ["ALL"] -"**/*_pb2*.py" = ["ALL"] -"**/__init__.py" = ["ALL"] +"**/__init__.py" = [ + "any-type", + "builtin-import-shadowing", + "implicit-namespace-package", + "import-private-name", + "invalid-module-name", + "non-empty-init-module", + "runtime-import-in-type-checking-block", + "undefined-export", + "undefined-local-with-import-star", + "undefined-local-with-import-star-usage", + "undocumented-public-init", + "unused-import", +] "**/tests/**" = [ "assert", "compare-to-empty-string", @@ -569,6 +582,9 @@ dependencies = ["flext-cli"] [tool.uv.exclude-dependencies.package] name = "flext-tests" +[tool.uv.exclude-newer-package] +mypy = "2026-08-15T03:04:00Z" + [tool.vulture] exclude = ["*/_protocols/*"] min_confidence = 100 @@ -620,7 +636,7 @@ dev = [ "mkdocs-versioning>=0.2", "mkdocstrings>=0.24", "mkdocstrings-python>=1.7", - "mypy>=2.3.0", + "mypy>=2.3.1", "pip-audit>=2.7.3", "pre-commit>=4.6.1", "psutil>=7.2.2", diff --git a/src/flext_cli/__init__.py b/src/flext_cli/__init__.py index 811e81e5b..7a2042736 100644 --- a/src/flext_cli/__init__.py +++ b/src/flext_cli/__init__.py @@ -1,71 +1,36 @@ -# @generated AUTO-GENERATED FILE — Regenerate with: make gen +# AUTO-GENERATED FILE — Regenerate with: make gen """Flext Cli package.""" from __future__ import annotations from typing import TYPE_CHECKING +from types import MappingProxyType + from flext_core.lazy import build_lazy_import_map, install_lazy_exports -from .__version__ import ( - __author__ as __author__, - __author_email__ as __author_email__, - __description__ as __description__, - __license__ as __license__, - __title__ as __title__, - __url__ as __url__, - __version__ as __version__, - __version_info__ as __version_info__, -) +from .__version__ import __author__ as __author__ +from .__version__ import __author_email__ as __author_email__ +from .__version__ import __description__ as __description__ +from .__version__ import __license__ as __license__ +from .__version__ import __title__ as __title__ +from .__version__ import __url__ as __url__ +from .__version__ import __version__ as __version__ +from .__version__ import __version_info__ as __version_info__ if TYPE_CHECKING: - from flext_core import d as d, e as e, h as h, r as r, x as x - - from ._config import FlextCliConfig as FlextCliConfig, config as config - from ._settings import FlextCliSettings as FlextCliSettings - from ._settings import settings as settings - from .api import FlextCli as FlextCli, cli as cli - from .base import ( - FlextCliServiceBase as FlextCliServiceBase, - FlextCliServiceBase as s, - ) - from .constants import ( - FlextCliConstants as FlextCliConstants, - FlextCliConstants as c, - ) - from .models import FlextCliModels as FlextCliModels, FlextCliModels as m - from .protocols import ( - FlextCliProtocols as FlextCliProtocols, - FlextCliProtocols as p, - ) - from .typings import FlextCliTypes as FlextCliTypes, FlextCliTypes as t - from .utilities import ( - FlextCliUtilities as FlextCliUtilities, - FlextCliUtilities as u, - ) - -_LAZY_MODULES: dict[str, tuple[str, ...]] = { - ".api": ("FlextCli", "cli"), - ".base": ("FlextCliServiceBase", "s"), - ".constants": ("FlextCliConstants", "c"), - ".models": ("FlextCliModels", "m"), - ".protocols": ("FlextCliProtocols", "p"), - "._config": ("FlextCliConfig", "config"), - "._settings": ("FlextCliSettings", "settings"), - ".typings": ("FlextCliTypes", "t"), - ".utilities": ("FlextCliUtilities", "u"), - "flext_core": ("d", "e", "h", "r", "x"), -} - - -_LAZY_ALIAS_GROUPS: dict[str, tuple[tuple[str, str], ...]] = {} + from flext_core import d, e, h, r, x - -_LAZY_IMPORTS = build_lazy_import_map( - _LAZY_MODULES, alias_groups=_LAZY_ALIAS_GROUPS, sort_keys=False -) - -_PUBLIC_EXPORTS: tuple[str, ...] = ( + from ._config import FlextCliConfig, config + from ._settings import FlextCliSettings, settings + from .api import FlextCli, cli + from .base import FlextCliServiceBase, FlextCliServiceBase as s + from .constants import FlextCliConstants, FlextCliConstants as c + from .models import FlextCliModels, FlextCliModels as m + from .protocols import FlextCliProtocols, FlextCliProtocols as p + from .typings import FlextCliTypes, FlextCliTypes as t + from .utilities import FlextCliUtilities, FlextCliUtilities as u +__all__: tuple[str, ...] = ( "FlextCli", "FlextCliConfig", "FlextCliConstants", @@ -99,6 +64,26 @@ "x", ) -__all__: tuple[str, ...] = tuple(_PUBLIC_EXPORTS) - -install_lazy_exports(__name__, globals(), _LAZY_IMPORTS, public_exports=__all__) +install_lazy_exports( + __name__, + globals(), + MappingProxyType( + build_lazy_import_map( + MappingProxyType({ + "._config": ("FlextCliConfig", "config"), + "._settings": ("FlextCliSettings", "settings"), + ".api": ("FlextCli", "cli"), + ".base": ("FlextCliServiceBase", "s"), + ".constants": ("FlextCliConstants", "c"), + ".models": ("FlextCliModels", "m"), + ".protocols": ("FlextCliProtocols", "p"), + ".typings": ("FlextCliTypes", "t"), + ".utilities": ("FlextCliUtilities", "u"), + "flext_core": ("d", "e", "h", "r", "x"), + }), + alias_groups=MappingProxyType({}), + sort_keys=False, + ) + ), + public_exports=__all__, +) diff --git a/src/flext_cli/_constants/xlsx.py b/src/flext_cli/_constants/xlsx.py index 3d02243fd..56ddc71fb 100644 --- a/src/flext_cli/_constants/xlsx.py +++ b/src/flext_cli/_constants/xlsx.py @@ -48,6 +48,8 @@ class FlextCliConstantsXlsx: ) XLSX_RECALC_SOURCE_NAME: Final[str] = "source.xlsx" XLSX_RECALC_TEMP_PREFIX: Final[str] = "flext-xlsx-recalc-" + XLSX_RECALC_PROFILE_DIR_NAME: Final[str] = "profile" + XLSX_RECALC_USER_PROFILE_ARGUMENT_PREFIX: Final[str] = "-env:UserInstallation=" XLSX_RECALC_TIMEOUT_SECONDS: Final[float] = 120.0 XLSX_RELATIONSHIPS_ID_ATTRIBUTE: Final[str] = ( "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id" diff --git a/src/flext_cli/_models/_xlsx/__init__.py b/src/flext_cli/_models/_xlsx/__init__.py index e69de29bb..db8d76cac 100644 --- a/src/flext_cli/_models/_xlsx/__init__.py +++ b/src/flext_cli/_models/_xlsx/__init__.py @@ -0,0 +1,67 @@ +# AUTO-GENERATED FILE — Regenerate with: make gen +"""Flext Cli. Models. Xlsx package.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from types import MappingProxyType + +from flext_core.lazy import build_lazy_import_map, install_lazy_exports + +if TYPE_CHECKING: + from .xlsx_archive import FlextCliModelsXlsxArchive + from .xlsx_cells import FlextCliModelsXlsxCells + from .xlsx_layout import FlextCliModelsXlsxLayout + from .xlsx_recalc import FlextCliModelsXlsxRecalc + from .xlsx_rules import FlextCliModelsXlsxRules + from .xlsx_snapshot import FlextCliModelsXlsxSnapshot + from .xlsx_style_catalog import FlextCliModelsXlsxStyleCatalog + from .xlsx_style_fills import FlextCliModelsXlsxStyleFills + from .xlsx_style_primitives import FlextCliModelsXlsxStylePrimitives + from .xlsx_styles import FlextCliModelsXlsxStyles + from .xlsx_tables import FlextCliModelsXlsxTables + from .xlsx_validation import FlextCliModelsXlsxValidation + from .xlsx_workbook import FlextCliModelsXlsxWorkbook +__all__: tuple[str, ...] = ( + "FlextCliModelsXlsxArchive", + "FlextCliModelsXlsxCells", + "FlextCliModelsXlsxLayout", + "FlextCliModelsXlsxRecalc", + "FlextCliModelsXlsxRules", + "FlextCliModelsXlsxSnapshot", + "FlextCliModelsXlsxStyleCatalog", + "FlextCliModelsXlsxStyleFills", + "FlextCliModelsXlsxStylePrimitives", + "FlextCliModelsXlsxStyles", + "FlextCliModelsXlsxTables", + "FlextCliModelsXlsxValidation", + "FlextCliModelsXlsxWorkbook", +) + +install_lazy_exports( + __name__, + globals(), + MappingProxyType( + build_lazy_import_map( + MappingProxyType({ + ".xlsx_archive": ("FlextCliModelsXlsxArchive",), + ".xlsx_cells": ("FlextCliModelsXlsxCells",), + ".xlsx_layout": ("FlextCliModelsXlsxLayout",), + ".xlsx_recalc": ("FlextCliModelsXlsxRecalc",), + ".xlsx_rules": ("FlextCliModelsXlsxRules",), + ".xlsx_snapshot": ("FlextCliModelsXlsxSnapshot",), + ".xlsx_style_catalog": ("FlextCliModelsXlsxStyleCatalog",), + ".xlsx_style_fills": ("FlextCliModelsXlsxStyleFills",), + ".xlsx_style_primitives": ("FlextCliModelsXlsxStylePrimitives",), + ".xlsx_styles": ("FlextCliModelsXlsxStyles",), + ".xlsx_tables": ("FlextCliModelsXlsxTables",), + ".xlsx_validation": ("FlextCliModelsXlsxValidation",), + ".xlsx_workbook": ("FlextCliModelsXlsxWorkbook",), + }), + alias_groups=MappingProxyType({}), + sort_keys=False, + ) + ), + public_exports=__all__, +) diff --git a/src/flext_cli/_utilities/_xlxx/xlsx_recalc.py b/src/flext_cli/_utilities/_xlxx/xlsx_recalc.py index d84889c29..bf2bc93b3 100644 --- a/src/flext_cli/_utilities/_xlxx/xlsx_recalc.py +++ b/src/flext_cli/_utilities/_xlxx/xlsx_recalc.py @@ -43,12 +43,21 @@ def _xlsx_recalc_unchecked( workdir = Path(workspace) input_dir = workdir / "input" output_dir = workdir / "output" + profile_dir = workdir / c.Cli.XLSX_RECALC_PROFILE_DIR_NAME input_dir.mkdir() output_dir.mkdir() + profile_dir.mkdir() source_path = input_dir / c.Cli.XLSX_RECALC_SOURCE_NAME source_path.write_bytes(request.source) + executable, *arguments = c.Cli.XLSX_RECALC_COMMAND started = FlextCliUtilitiesProcesses.process_start( - (*c.Cli.XLSX_RECALC_COMMAND, str(output_dir), str(source_path)), + ( + executable, + f"{c.Cli.XLSX_RECALC_USER_PROFILE_ARGUMENT_PREFIX}{profile_dir.as_uri()}", + *arguments, + str(output_dir), + str(source_path), + ), cwd=workdir, ) if started.failure: diff --git a/src/flext_cli/settings.py b/src/flext_cli/settings.py deleted file mode 100644 index 233f7fc43..000000000 --- a/src/flext_cli/settings.py +++ /dev/null @@ -1,83 +0,0 @@ -"""FLEXT CLI Configuration Module. - -CLI-specific settings extending FlextSettings. All Pydantic v2; no compatibility layers. - -Copyright (c) 2025 FLEXT Team. All rights reserved. -SPDX-License-Identifier: MIT -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Annotated - -from flext_cli import c -from flext_core import FlextSettings, m, u - -if TYPE_CHECKING: - from flext_cli import p - - -class FlextCliSettings(FlextSettings): - """CLI-specific configuration; extends FlextSettings with profile and CLI fields.""" - - model_config = m.SettingsConfigDict(env_prefix="FLEXT_CLI_", extra="ignore") - - class CliSettings(m.SettingsValue): - """Namespaced CLI runtime settings.""" - - verbose: Annotated[bool, m.Field(description="Verbose output")] = ( - c.Cli.CLI_DEFAULT_VERBOSE - ) - quiet: Annotated[bool, m.Field(description="Quiet output")] = ( - c.Cli.CLI_DEFAULT_QUIET - ) - app_name: Annotated[str, m.Field(description="CLI application name")] = ( - c.Cli.FLEXT_CLI - ) - log_verbosity: Annotated[ - str, m.Field(description="Log format (compact, detailed, full)") - ] = c.Cli.LogVerbosity.COMPACT - cli_log_level: Annotated[ - c.LogLevel | str, m.Field(description="CLI log level") - ] = c.LogLevel.INFO - no_color: Annotated[bool, m.Field(description="Disable colored output")] = ( - c.Cli.CLI_DEFAULT_NO_COLOR - ) - output_format: Annotated[ - str, m.Field(description="Output format (table, json, yaml, csv, plain)") - ] = c.Cli.OUTPUT_DEFAULT_FORMAT_TYPE - config_file: Annotated[ - str | None, m.Field(description="Path to settings file") - ] = None - token_file: Annotated[ - str | None, m.Field(description="Path to auth token file") - ] = None - ci: Annotated[ - bool, - m.Field(description="Whether the current runtime is a CI environment."), - ] = c.Cli.ENV_DEFAULT_CI - pytest_current_test: Annotated[ - str | None, m.Field(description="Current pytest test identifier.") - ] = None - shell_command: Annotated[ - str | None, - m.Field(description="Current shell command propagated by the runtime."), - ] = None - - @u.computed_field - @property - def test_env(self) -> bool: - """Whether prompts should treat the current runtime as test/CI mode.""" - normalized_shell = (self.shell_command or "").strip().lower() - return ( - self.pytest_current_test is not None - or "pytest" in normalized_shell - or self.ci - ) - - if TYPE_CHECKING: - Cli: p.Cli.CliSettings - else: - Cli: CliSettings = m.Field( - default_factory=CliSettings, description="Namespaced CLI settings branch." - ) diff --git a/tests/_exports_public.py b/tests/_exports_public.py index aab25afee..fd0df500a 100644 --- a/tests/_exports_public.py +++ b/tests/_exports_public.py @@ -68,7 +68,6 @@ "make_prompts", "p", "r", - "reset_settings", "s", "t", "td", diff --git a/tests/test_xlsx_recalc.py b/tests/test_xlsx_recalc.py index c8fe1db30..886d8ebda 100644 --- a/tests/test_xlsx_recalc.py +++ b/tests/test_xlsx_recalc.py @@ -2,6 +2,10 @@ from __future__ import annotations +from concurrent.futures import ThreadPoolExecutor + +import pytest + from flext_cli import cli, m, p from flext_tests import tm @@ -113,3 +117,16 @@ def test_xlsx_recalc_parity_detects_count_mismatch() -> None: tm.that(report.success, eq=True, msg=report.error) tm.that(report.value.formula_count, eq=2) tm.that(report.value.ok, eq=False) + + +@pytest.mark.slow +def test_xlsx_recalc_supports_concurrent_public_calls() -> None: + """Concurrent callers receive independently recalculated workbooks.""" + source = _render_workbook() + request = m.Cli.XlsxRecalcRequest(source=source) + with ThreadPoolExecutor(max_workers=3) as executor: + results = tuple(executor.map(lambda _index: cli.xlsx_recalc(request), range(3))) + for result in results: + tm.that(result.success, eq=True, msg=result.error) + value = _numeric_cell_value(result.value.content, "Report", "A1") + tm.that(value.value, eq=5) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 8b0870db8..3ba46b815 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -8,7 +8,6 @@ from flext_cli import FlextCliSettings from flext_cli.services.prompts import FlextCliPrompts -from flext_tests import reset_settings from tests import m if TYPE_CHECKING: @@ -144,5 +143,4 @@ def pytest_runtest_teardown(item: pytest.Item, nextitem: pytest.Item | None) -> "make_capture_prompts", "make_failing_prompts", "make_prompts", - "reset_settings", ] diff --git a/tests/unit/test_params_branch_cov.py b/tests/unit/test_params_branch_cov.py index 6a5cc2893..e01b96604 100644 --- a/tests/unit/test_params_branch_cov.py +++ b/tests/unit/test_params_branch_cov.py @@ -9,7 +9,7 @@ import pytest -from flext_cli import FlextCliSettings, c, m, p, u +from flext_cli import c, m, p, settings as cli_settings, u from flext_tests import tm from tests import c as tc @@ -50,7 +50,7 @@ def test_resolve_is_idempotent_for_same_inputs(self) -> None: def test_set_bool_applies_root_and_cli_flags(self) -> None: """Verify that set bool applies root and cli flags.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() params = m.Cli.CliParamsConfig( debug=True, trace=True, verbose=True, quiet=True, no_color=True ) @@ -64,7 +64,7 @@ def test_set_bool_applies_root_and_cli_flags(self) -> None: def test_set_bool_trace_without_debug_fails(self) -> None: """Verify that set bool trace without debug fails.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() params = m.Cli.CliParamsConfig(trace=True) result = u.Cli.params_set_bool(settings, params) tm.fail(result) @@ -72,7 +72,7 @@ def test_set_bool_trace_without_debug_fails(self) -> None: def test_set_bool_no_flags_returns_settings_unchanged(self) -> None: """Verify that set bool no flags returns settings unchanged.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() result = u.Cli.params_set_bool(settings, m.Cli.CliParamsConfig()) tm.ok(result) tm.that(result.value.debug is settings.debug, eq=True) @@ -83,7 +83,7 @@ def test_set_bool_no_flags_returns_settings_unchanged(self) -> None: @pytest.mark.parametrize("level", ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]) def test_set_log_level_applies_valid_level(self, level: str) -> None: """Verify that set log level applies valid level.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() params = m.Cli.CliParamsConfig(log_level=level) result = u.Cli.params_set_log_level(settings, params) tm.ok(result) @@ -91,14 +91,14 @@ def test_set_log_level_applies_valid_level(self, level: str) -> None: def test_set_log_level_none_returns_settings_unchanged(self) -> None: """Verify that set log level none returns settings unchanged.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() result = u.Cli.params_set_log_level(settings, m.Cli.CliParamsConfig()) tm.ok(result) tm.that(result.value.cli_log_level, eq=settings.cli_log_level) def test_set_log_level_invalid_fails_with_options_message(self) -> None: """Verify that set log level invalid fails with options message.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() params = m.Cli.CliParamsConfig(log_level="BOGUS") result = u.Cli.params_set_log_level(settings, params) tm.fail(result) @@ -114,7 +114,7 @@ def test_set_log_level_invalid_fails_with_options_message(self) -> None: @pytest.mark.parametrize("log_format", ["compact", "detailed", "full"]) def test_set_format_applies_valid_log_format(self, log_format: str) -> None: """Verify that set format applies valid log format.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() params = m.Cli.CliParamsConfig(log_format=log_format) result = u.Cli.params_set_format(settings, params) tm.ok(result) @@ -125,7 +125,7 @@ def test_set_format_applies_valid_log_format(self, log_format: str) -> None: ) def test_set_format_applies_valid_output_format(self, output_format: str) -> None: """Verify that set format applies valid output format.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() params = m.Cli.CliParamsConfig(output_format=output_format) result = u.Cli.params_set_format(settings, params) tm.ok(result) @@ -133,7 +133,7 @@ def test_set_format_applies_valid_output_format(self, output_format: str) -> Non def test_set_format_none_returns_settings_unchanged(self) -> None: """Verify that set format none returns settings unchanged.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() result = u.Cli.params_set_format(settings, m.Cli.CliParamsConfig()) tm.ok(result) tm.that(result.value.cli_log_verbosity, eq=settings.cli_log_verbosity) @@ -141,7 +141,7 @@ def test_set_format_none_returns_settings_unchanged(self) -> None: def test_set_format_invalid_log_format_fails(self) -> None: """Verify that set format invalid log format fails.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() params = m.Cli.CliParamsConfig(log_format="BAD") result = u.Cli.params_set_format(settings, params) tm.fail(result) @@ -154,7 +154,7 @@ def test_set_format_invalid_log_format_fails(self) -> None: def test_set_format_invalid_output_format_fails(self) -> None: """Verify that set format invalid output format fails.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() params = m.Cli.CliParamsConfig(output_format="BAD") result = u.Cli.params_set_format(settings, params) tm.fail(result) @@ -169,7 +169,7 @@ def test_set_format_invalid_output_format_fails(self) -> None: def test_apply_chains_all_stages_on_valid_params(self) -> None: """Verify that apply chains all stages on valid params.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() params = m.Cli.CliParamsConfig( debug=True, log_level="INFO", output_format="yaml", log_format="detailed" ) @@ -183,7 +183,7 @@ def test_apply_chains_all_stages_on_valid_params(self) -> None: def test_apply_short_circuits_on_first_stage_failure(self) -> None: """Verify that apply short circuits on first stage failure.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() params = m.Cli.CliParamsConfig(trace=True) result = u.Cli.params_apply(settings, params) tm.fail(result) @@ -191,7 +191,7 @@ def test_apply_short_circuits_on_first_stage_failure(self) -> None: def test_apply_returns_result_type(self) -> None: """Verify that apply returns result type.""" - settings = FlextCliSettings.model_validate({}) + settings = cli_settings.clone() result = u.Cli.params_apply(settings, m.Cli.CliParamsConfig()) tm.that(result, is_=p.Result) tm.ok(result) diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index e47653d95..9cc8784e0 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -6,7 +6,7 @@ application, ``model_dump`` shape, and the ``fetch_global`` singleton / ``reset_for_testing`` isolation contracts. -Modules tested: flext_cli.settings.FlextCliSettings, flext_cli.settings.settings +Modules tested: flext_cli.FlextCliSettings, flext_cli.settings Copyright (c) 2025 FLEXT Team. All rights reserved. SPDX-License-Identifier: MIT @@ -88,7 +88,7 @@ def test_cli_test_env_truth_table( expected: bool, ) -> None: """cli_test_env is true iff pytest markers or CI mode are present.""" - built = FlextCliSettings.model_validate({ + built: p.Cli.Settings = FlextCliSettings.model_validate({ "cli_pytest_current_test": pytest_current_test, "cli_shell_command": shell_command, "cli_ci": ci, @@ -111,7 +111,10 @@ def test_log_verbosity_preserves_each_mode( def test_model_validate_applies_flat_overrides(self) -> None: """Partial model_validate applies flat overrides onto defaults.""" - built = FlextCliSettings.model_validate({"cli_verbose": True, "cli_ci": True}) + built: p.Cli.Settings = FlextCliSettings.model_validate({ + "cli_verbose": True, + "cli_ci": True, + }) tm.that(built.cli_verbose, eq=True) tm.that(built.cli_ci, eq=True) tm.that(u.Cli.cli_test_env(built), eq=True) diff --git a/tests/unit/test_yaml_roundtrip.py b/tests/unit/test_yaml_roundtrip.py index a87cb42c0..ad3bcbd2b 100644 --- a/tests/unit/test_yaml_roundtrip.py +++ b/tests/unit/test_yaml_roundtrip.py @@ -116,6 +116,7 @@ def test_yaml_parse_empty_document_fails_without_exception(self) -> None: tm.fail(result) tm.that(result.error, none=False) + @pytest.mark.slow def test_roundtrip_load_text_is_thread_safe(self) -> None: """Verify that roundtrip load text is thread safe.""" documents = [ diff --git a/tests/utilities.py b/tests/utilities.py index 7b2211420..a16ffeda0 100644 --- a/tests/utilities.py +++ b/tests/utilities.py @@ -14,8 +14,7 @@ from flext_cli import cli, u from flext_tests import FlextTestsUtilities, r -from tests import c, p -from tests.settings import TestsFlextCliSettings +from tests import TestsFlextCliSettings, c, p class TestsFlextCliUtilities(FlextTestsUtilities, u): @@ -27,7 +26,8 @@ class Tests(FlextTestsUtilities.Tests): @staticmethod def create_test_settings() -> p.Result[p.Cli.Settings]: """Create test settings via Railway pattern.""" - return r[p.Cli.Settings].ok(TestsFlextCliSettings()) + settings: p.Cli.Settings = TestsFlextCliSettings() + return r[p.Cli.Settings].ok(settings) @staticmethod def create_cli_app() -> p.Result[p.Cli.Application]: