From 04fe21e16fbdc94e80794e43225ee46f6f685b5c Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Fri, 14 Aug 2026 14:35:57 -0300 Subject: [PATCH 01/14] fix(generated): repair pytest conflict projection --- Makefile | 57 ++++++++++++++++++++++++++++++++++++-------------- pyproject.toml | 4 ---- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 97820c426..2f21143c1 100644 --- a/Makefile +++ b/Makefile @@ -61,7 +61,7 @@ override PYTEST_ENFORCEMENT_PLUGIN := flext_tests_enforcement override PYTEST_PROGRESS_ARGS := --verbose override PYTEST_REPORT_ARGS := -ra --durations=25 --durations-min=0.001 --tb=short override PYTEST_DIAG_ARGS := -rA --durations=0 --tb=long --showlocals -override PYTEST_PARALLEL_WORKERS := 8 +override PYTEST_PARALLEL_WORKERS := 12 override PYTEST_PARALLEL_DISTRIBUTION := worksteal override PYTEST_PROFILE_SORT := cumulative override PYTEST_PROFILE_LIMIT := 50 @@ -91,10 +91,10 @@ MAKEFILE_ROOT := $(patsubst %/,%,$(dir $(SELF_MAKEFILE))) PROJECT_ROOT := $(MAKEFILE_ROOT) override export FLEXT_PYTEST_TARGET_RAW := tests WORKSPACE ?= $(PROJECT_ROOT) -# make work targets a member checkout when PROJECT names a workspace member and -# WORKSPACE was not overridden on the command line. PROJECT alone used to keep -# WORKSPACE at the workspace root, so finish looked up lanes in the wrong git -# primary and failed with "worktree branch is not registered". +# 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)),) @@ -102,6 +102,7 @@ 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 @@ -127,7 +128,7 @@ _ALLOWED_WHATS_setup := environment $(shell sed -n 's/^_custom_setup_\([a-z0-9_- _ALLOWED_WHATS_deps := check lock upgrade $(shell sed -n 's/^_custom_deps_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') _ALLOWED_WHATS_build := artifacts $(shell sed -n 's/^_custom_build_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') _ALLOWED_WHATS_check := all $(shell sed -n 's/^_custom_check_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') -_ALLOWED_WHATS_test := all full cache-status cache-clear cache-checkpoint $(shell sed -n 's/^_custom_test_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') +_ALLOWED_WHATS_test := all full profile cache-status cache-clear cache-checkpoint $(shell sed -n 's/^_custom_test_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') _ALLOWED_WHATS_fmt := check all apply $(shell sed -n 's/^_custom_fmt_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') _ALLOWED_WHATS_fix := check all apply $(shell sed -n 's/^_custom_fix_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') _ALLOWED_WHATS_run := default $(shell sed -n 's/^_custom_run_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') @@ -355,7 +356,7 @@ endif # stayed behind — and a generated project never includes base.mk, so the # monopoly was unenforced in every real checkout. The guard belongs with the # verbs it protects. -CUSTOM_MK_RESERVED_TARGETS := _custom_build_artifacts _custom_check_all _custom_clean_generated _custom_clean_status _custom_deps_check _custom_deps_lock _custom_deps_upgrade _custom_fix_all _custom_fix_apply _custom_fix_check _custom_fmt_all _custom_fmt_apply _custom_fmt_check _custom_gen_all _custom_gen_apply _custom_gen_check _custom_help_usage _custom_mod_all _custom_mod_apply _custom_mod_check _custom_release_rel _custom_release_status _custom_run_default _custom_setup_environment _custom_status_diagnostics _custom_test_all _custom_test_cache-checkpoint _custom_test_cache-clear _custom_test_cache-status _custom_test_full _custom_work_finish _custom_work_land _custom_work_start _custom_work_status build check clean deps fix fmt gen help mod release run setup status test work +CUSTOM_MK_RESERVED_TARGETS := _custom_build_artifacts _custom_check_all _custom_clean_generated _custom_clean_status _custom_deps_check _custom_deps_lock _custom_deps_upgrade _custom_fix_all _custom_fix_apply _custom_fix_check _custom_fmt_all _custom_fmt_apply _custom_fmt_check _custom_gen_all _custom_gen_apply _custom_gen_check _custom_help_usage _custom_mod_all _custom_mod_apply _custom_mod_check _custom_release_rel _custom_release_status _custom_run_default _custom_setup_environment _custom_status_diagnostics _custom_test_all _custom_test_cache-checkpoint _custom_test_cache-clear _custom_test_cache-status _custom_test_full _custom_test_profile _custom_work_finish _custom_work_land _custom_work_start _custom_work_status build check clean deps fix fmt gen help mod release run setup status test work ifneq ($(wildcard custom.mk),) # Target definitions at column 0, excluding assignments (=) and dot-directives. # $(shell) converts the newline-separated results to space-separated lists. @@ -431,7 +432,7 @@ define _run_for_selected_projects done endef -.PHONY: $(PUBLIC_VERBS) _builtin_help_usage _builtin_setup_environment _builtin_deps_check _builtin_deps_lock _builtin_deps_upgrade _builtin_build_artifacts _builtin_check_all _builtin_test_all _builtin_test_full _builtin_test_cache-status _builtin_test_cache-clear _builtin_test_cache-checkpoint _builtin_fmt_check _builtin_fmt_all _builtin_fmt_apply _builtin_fix_check _builtin_fix_all _builtin_fix_apply _builtin_run_default _builtin_status_diagnostics _builtin_clean_status _builtin_clean_generated _builtin_release_status _builtin_release_rel _builtin_gen_check _builtin_gen_all _builtin_gen_apply _builtin_work_start _builtin_work_status _builtin_work_land _builtin_work_finish _builtin_mod_check _builtin_mod_all _builtin_mod_apply +.PHONY: $(PUBLIC_VERBS) _builtin_help_usage _builtin_setup_environment _builtin_deps_check _builtin_deps_lock _builtin_deps_upgrade _builtin_build_artifacts _builtin_check_all _builtin_test_all _builtin_test_full _builtin_test_profile _builtin_test_cache-status _builtin_test_cache-clear _builtin_test_cache-checkpoint _builtin_fmt_check _builtin_fmt_all _builtin_fmt_apply _builtin_fix_check _builtin_fix_all _builtin_fix_apply _builtin_run_default _builtin_status_diagnostics _builtin_clean_status _builtin_clean_generated _builtin_release_status _builtin_release_rel _builtin_gen_check _builtin_gen_all _builtin_gen_apply _builtin_work_start _builtin_work_status _builtin_work_land _builtin_work_finish _builtin_mod_check _builtin_mod_all _builtin_mod_apply # Every public verb dispatches straight into its private builtin. The verbs # that used to round-trip through the Python serializer keep the environment @@ -595,6 +596,9 @@ _builtin_help_usage: # work is carried. Pin validity is HEAD contains gitlink — origin may lag the # pin without failing verify. Declared branch is the named integration line; # legacy branch=. still resolves to the superproject named branch if present. +# A checkout is also accepted on the superproject current branch (workspace +# lane): that lane branch then becomes the verified branch, and its fetch is +# skipped when origin carries no counterpart. Any third branch still fails. # Fetch skips when local already contains pin and origin tip. # Free: no # End SECTION: submodule setup @@ -659,13 +663,19 @@ _builtin_setup_submodules: printf 'ERROR: governed gitlink has no declared branch: %s\n' "$$child_path" >&2; \ exit 2; \ fi; \ + super_branch=$$(git -C "$$superproject" branch --show-current); \ if [ "$$branch" = "." ]; then \ - branch=$$(git -C "$$superproject" branch --show-current); \ + branch="$$super_branch"; \ if [ -z "$$branch" ]; then \ printf 'ERROR: %s: branch = . requires a named superproject branch\n' "$$child_path" >&2; \ exit 1; \ fi; \ fi; \ + declared_branch="$$branch"; \ + accepted_branches="$$declared_branch"; \ + if [ -n "$$super_branch" ] && [ "$$super_branch" != "$$declared_branch" ]; then \ + accepted_branches="$$declared_branch or $$super_branch"; \ + fi; \ git check-ref-format --branch "$$branch" >/dev/null || { \ printf 'ERROR: %s: invalid declared branch %s\n' "$$child_path" "$$branch" >&2; \ exit 1; \ @@ -682,11 +692,15 @@ _builtin_setup_submodules: }; \ attach_branch_at_head "$$child_root" "$$branch"; \ fi; \ - remote_ref="refs/remotes/origin/$$branch"; \ current=$$(git -C "$$child_root" branch --show-current); \ + if [ -n "$$current" ] && [ "$$current" != "$$declared_branch" ] && \ + [ -n "$$super_branch" ] && [ "$$current" = "$$super_branch" ]; then \ + branch="$$super_branch"; \ + fi; \ + remote_ref="refs/remotes/origin/$$branch"; \ head=$$(git -C "$$child_root" rev-parse HEAD); \ if [ -n "$$current" ] && [ "$$current" != "$$branch" ]; then \ - printf 'ERROR: %s: conflicting branch %s; expected %s (setup never runs checkout/reset; switch it yourself while keeping dirty)\n' "$$child_path" "$$current" "$$branch" >&2; \ + printf 'ERROR: %s: conflicting branch %s; expected %s (setup never runs checkout/reset; switch it yourself while keeping dirty)\n' "$$child_path" "$$current" "$$accepted_branches" >&2; \ exit 1; \ fi; \ need_fetch=1; \ @@ -702,15 +716,22 @@ _builtin_setup_submodules: fi; \ fi; \ if [ "$$need_fetch" -eq 1 ]; then \ - git -C "$$child_root" fetch --quiet origin "$$branch" || { \ - printf 'ERROR: %s: fetch origin %s failed\n' "$$child_path" "$$branch" >&2; \ - exit 1; \ - }; \ + fetch_allowed=1; \ + if [ "$$branch" != "$$declared_branch" ] && \ + ! git -C "$$child_root" ls-remote --exit-code --heads origin "$$branch" >/dev/null 2>&1; then \ + fetch_allowed=0; \ + fi; \ + if [ "$$fetch_allowed" -eq 1 ]; then \ + git -C "$$child_root" fetch --quiet origin "$$branch" || { \ + printf 'ERROR: %s: fetch origin %s failed\n' "$$child_path" "$$branch" >&2; \ + exit 1; \ + }; \ + fi; \ fi; \ current=$$(git -C "$$child_root" branch --show-current); \ head=$$(git -C "$$child_root" rev-parse HEAD); \ if [ -n "$$current" ] && [ "$$current" != "$$branch" ]; then \ - printf 'ERROR: %s: conflicting branch %s; expected %s (setup never runs checkout/reset; switch it yourself while keeping dirty)\n' "$$child_path" "$$current" "$$branch" >&2; \ + printf 'ERROR: %s: conflicting branch %s; expected %s (setup never runs checkout/reset; switch it yourself while keeping dirty)\n' "$$child_path" "$$current" "$$accepted_branches" >&2; \ exit 1; \ fi; \ if [ -z "$$current" ]; then \ @@ -866,6 +887,10 @@ _builtin_test_full: _builtin_require_environment @$(PYTEST_BOUNDED) $(UV_RUN) python -m flext_infra._pytest_entry +_builtin_test_profile: _builtin_require_environment + + @$(PYTEST_BOUNDED) $(UV_RUN) python -m flext_infra._pytest_entry + _builtin_test_cache-status: _builtin_require_environment @$(PYTEST_BOUNDED) $(UV_RUN) python -m flext_infra._pytest_entry diff --git a/pyproject.toml b/pyproject.toml index b997e0a15..e307bed1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -393,11 +393,7 @@ addopts = [ "--durations=10", "--markdown-docs", "--strict-markers", -<<<<<<< HEAD - "--timeout=90", -======= "--timeout=10", ->>>>>>> refs/remotes/origin/0.12.0-dev ] filterwarnings = [ "error", From bd1c7bda8128b38b42cda3cc24c3ced4d28d370b Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Fri, 14 Aug 2026 14:41:06 -0300 Subject: [PATCH 02/14] fix(generated): repair fleet pytest projection --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index e307bed1b..2b068c129 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -399,6 +399,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", From f06ee4f24d144d92a181b5d6842697b842d72956 Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Fri, 14 Aug 2026 15:04:52 -0300 Subject: [PATCH 03/14] fix(generated): keep generation single-owner --- Makefile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 2f21143c1..dd2dd7a93 100644 --- a/Makefile +++ b/Makefile @@ -991,17 +991,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) 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) deps modernize --workspace "$(PROJECT_ROOT)" --apply _builtin_gen_apply: _builtin_gen_all From 17beb0f5fbc4e88d95264f02ac47239d2793a72f Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sat, 15 Aug 2026 14:25:03 -0300 Subject: [PATCH 04/14] chore(generated): align Make surface --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dd2dd7a93..e1a9a5bb5 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ override PYTEST_PROGRESS_ARGS := --verbose override PYTEST_REPORT_ARGS := -ra --durations=25 --durations-min=0.001 --tb=short override PYTEST_DIAG_ARGS := -rA --durations=0 --tb=long --showlocals override PYTEST_PARALLEL_WORKERS := 12 -override PYTEST_PARALLEL_DISTRIBUTION := worksteal +override PYTEST_PARALLEL_DISTRIBUTION := loadgroup override PYTEST_PROFILE_SORT := cumulative override PYTEST_PROFILE_LIMIT := 50 override PROCESS_TIMEOUT_COMMAND := timeout From 2ebfc005997d1c887e89c264885295f9b84c1d7a Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sat, 15 Aug 2026 14:27:30 -0300 Subject: [PATCH 05/14] fix(xlsx): isolate the LibreOffice user profile per recalc workspace Concurrent recalc calls collided on the shared LibreOffice profile lock. Each workspace now gets its own -env:UserInstallation directory, proven by a concurrent public-call regression test. Subprocess-heavy tests use the config-owned slow budget. --- src/flext_cli/_constants/xlsx.py | 4 ++++ src/flext_cli/_utilities/_xlxx/xlsx_recalc.py | 11 ++++++++++- tests/test_xlsx_recalc.py | 17 +++++++++++++++++ tests/unit/test_yaml_roundtrip.py | 1 + 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/flext_cli/_constants/xlsx.py b/src/flext_cli/_constants/xlsx.py index 3d02243fd..84b154d09 100644 --- a/src/flext_cli/_constants/xlsx.py +++ b/src/flext_cli/_constants/xlsx.py @@ -48,6 +48,10 @@ 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/_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/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/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 = [ From b3fc64dacdb8ff9fa9e918b9b8002c4c6561f1f9 Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sat, 15 Aug 2026 14:45:29 -0300 Subject: [PATCH 06/14] style(xlsx): apply canonical formatter to recalc constants --- src/flext_cli/_constants/xlsx.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/flext_cli/_constants/xlsx.py b/src/flext_cli/_constants/xlsx.py index 84b154d09..56ddc71fb 100644 --- a/src/flext_cli/_constants/xlsx.py +++ b/src/flext_cli/_constants/xlsx.py @@ -49,9 +49,7 @@ 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_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" From 06d7a3fe775597e6781fb9d425d14870a107994d Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sat, 15 Aug 2026 17:42:41 -0300 Subject: [PATCH 07/14] chore(generated): align init-module lint policy --- pyproject.toml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2b068c129..9b73d99c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -516,20 +516,7 @@ split-on-trailing-comma = false [tool.ruff.lint.per-file-ignores] "**/.vulture_whitelist.py" = ["ALL"] "**/*_pb2*.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", -] +"**/__init__.py" = ["ALL"] "**/tests/**" = [ "assert", "compare-to-empty-string", From a522a2caa66164b388eda7fd3a5c0ae829a53893 Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sat, 15 Aug 2026 22:10:49 -0300 Subject: [PATCH 08/14] chore(generated): project merged tooling contract --- .github/workflows/docs.yml | 12 ++++++++---- Makefile | 6 +++--- pyproject.toml | 16 ++++++++++++++-- 3 files changed, 25 insertions(+), 9 deletions(-) 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/Makefile b/Makefile index e1a9a5bb5..bcce344c4 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 @@ -62,7 +62,7 @@ override PYTEST_PROGRESS_ARGS := --verbose override PYTEST_REPORT_ARGS := -ra --durations=25 --durations-min=0.001 --tb=short override PYTEST_DIAG_ARGS := -rA --durations=0 --tb=long --showlocals override PYTEST_PARALLEL_WORKERS := 12 -override PYTEST_PARALLEL_DISTRIBUTION := loadgroup +override PYTEST_PARALLEL_DISTRIBUTION := worksteal override PYTEST_PROFILE_SORT := cumulative override PYTEST_PROFILE_LIMIT := 50 override PROCESS_TIMEOUT_COMMAND := timeout diff --git a/pyproject.toml b/pyproject.toml index 9b73d99c7..e307bed1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -399,7 +399,6 @@ filterwarnings = [ "error", "module::flext_core._constants.enforcement.FlextMroViolation", ] -flext_slow_timeout_seconds = 60 markers = [ "docker: tests requiring Docker", "e2e: end-to-end integration tests", @@ -516,7 +515,20 @@ 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", From e7403bf2535bd752ad5c0410c86130b95e421c97 Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sun, 16 Aug 2026 02:08:50 -0300 Subject: [PATCH 09/14] chore(generated): align init-module lint policy with the scoped owner --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e307bed1b..582ef0bd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -513,8 +513,6 @@ 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" = [ "any-type", "builtin-import-shadowing", From b44f4361a296773daf9cfad712cdf5f0e2d118e4 Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sun, 16 Aug 2026 02:15:51 -0300 Subject: [PATCH 10/14] fix(tests): drop the retired reset_settings re-export from the CLI conftest --- tests/_exports_public.py | 1 - tests/unit/conftest.py | 2 -- 2 files changed, 3 deletions(-) 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/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", ] From 300d6ffbc93a493e39d602c720c4e4d72a321fbd Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sun, 16 Aug 2026 02:52:13 -0300 Subject: [PATCH 11/14] chore(generated): adopt the config-owned slow-item budget --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 582ef0bd7..12a46b5bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -399,6 +399,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", From e3d0ef94fd5bc4199d344cd30bed9ac24a476c57 Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sun, 16 Aug 2026 03:55:42 -0300 Subject: [PATCH 12/14] chore(generated): regenerate lazy package initializers --- src/flext_cli/__init__.py | 107 +++++++++++------------- src/flext_cli/_models/_xlsx/__init__.py | 67 +++++++++++++++ 2 files changed, 114 insertions(+), 60 deletions(-) diff --git a/src/flext_cli/__init__.py b/src/flext_cli/__init__.py index 811e81e5b..348bb1d60 100644 --- a/src/flext_cli/__init__.py +++ b/src/flext_cli/__init__.py @@ -1,71 +1,37 @@ -# @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 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 .settings import FlextCliSettings + from .typings import FlextCliTypes, FlextCliTypes as t + from .utilities import FlextCliUtilities, FlextCliUtilities as u +__all__: tuple[str, ...] = ( "FlextCli", "FlextCliConfig", "FlextCliConstants", @@ -99,6 +65,27 @@ "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": ("settings",), + ".api": ("FlextCli", "cli"), + ".base": ("FlextCliServiceBase", "s"), + ".constants": ("FlextCliConstants", "c"), + ".models": ("FlextCliModels", "m"), + ".protocols": ("FlextCliProtocols", "p"), + ".settings": ("FlextCliSettings",), + ".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/_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__, +) From a8573e05c4725d5b03406eb1da81ca6e6c2cefe8 Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sun, 16 Aug 2026 13:07:10 -0300 Subject: [PATCH 13/14] chore(generated): regenerate flext-cli projections make gen WHAT=apply APPLY=Y projections for flext-cli lane. Fixed-point verified via make gen WHAT=check. --- .markdownlint.json | 38 +++++++++++++++--- .markdownlintignore | 24 +++++++++++ Makefile | 54 +++++++++++++++---------- pyproject.toml | 6 ++- src/flext_cli/settings.py | 83 --------------------------------------- 5 files changed, 96 insertions(+), 109 deletions(-) create mode 100644 .markdownlintignore delete mode 100644 src/flext_cli/settings.py 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 bcce344c4..99965f6d3 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 12a46b5bd..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 @@ -581,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 @@ -632,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/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." - ) From b048f9b322916a8fec5f0556cf8b38f053fb8acd Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sun, 16 Aug 2026 13:07:32 -0300 Subject: [PATCH 14/14] fix(settings): cutover FlextCliSettings to canonical _settings owner The nested legacy src/flext_cli/settings.py is superseded by the flat FlextCliSettings in src/flext_cli/_settings.py (runtime-first). Public facade now exports _settings.FlextCliSettings; tests and docs adjusted to the flat contract. Zero old-path consumers remain. --- docs/architecture.md | 4 ++-- docs/development.md | 2 +- src/flext_cli/__init__.py | 6 ++---- tests/unit/test_params_branch_cov.py | 30 ++++++++++++++-------------- tests/unit/test_settings.py | 9 ++++++--- tests/utilities.py | 6 +++--- 6 files changed, 29 insertions(+), 28 deletions(-) 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/src/flext_cli/__init__.py b/src/flext_cli/__init__.py index 348bb1d60..7a2042736 100644 --- a/src/flext_cli/__init__.py +++ b/src/flext_cli/__init__.py @@ -22,13 +22,12 @@ from flext_core import d, e, h, r, x from ._config import FlextCliConfig, config - from ._settings import settings + 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 .settings import FlextCliSettings from .typings import FlextCliTypes, FlextCliTypes as t from .utilities import FlextCliUtilities, FlextCliUtilities as u __all__: tuple[str, ...] = ( @@ -72,13 +71,12 @@ build_lazy_import_map( MappingProxyType({ "._config": ("FlextCliConfig", "config"), - "._settings": ("settings",), + "._settings": ("FlextCliSettings", "settings"), ".api": ("FlextCli", "cli"), ".base": ("FlextCliServiceBase", "s"), ".constants": ("FlextCliConstants", "c"), ".models": ("FlextCliModels", "m"), ".protocols": ("FlextCliProtocols", "p"), - ".settings": ("FlextCliSettings",), ".typings": ("FlextCliTypes", "t"), ".utilities": ("FlextCliUtilities", "u"), "flext_core": ("d", "e", "h", "r", "x"), 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/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]: