From be5c5747a9b67967d2443cb8f2006b6c6c011bd5 Mon Sep 17 00:00:00 2001 From: Anh Tran Nguyen Minh Date: Sun, 30 Aug 2026 11:25:27 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20gom=20i18n/=20v=C3=A0=20theme/=20th?= =?UTF-8?q?=C3=A0nh=20g=C3=B3i,=20g=E1=BB=99p=20requirements=20v=E1=BB=81?= =?UTF-8?q?=20m=E1=BB=99t=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thư mục gốc: 22 file .py -> 7 -------------------------------- 13 file "thành phần" nằm rải rác ngay ngoài thư mục gốc, mỗi file chỉ có ĐÚNG MỘT nơi import — chính cái hub của nó: i18n.py + 10 file i18n_*.py -> i18n/__init__.py + i18n/*.py theme.py + 3 file theme_*.py -> theme/__init__.py + theme/*.py Đổi hub thành `__init__.py` nên 78 chỗ `from ..i18n import tr` và 24 chỗ `from ..theme import current_palette` KHÔNG phải sửa một dòng nào. Git nhận ra 11/15 file là đổi tên thuần, 0 dòng thay đổi; 4 file còn lại chỉ sửa đúng dòng import và mấy tham chiếu tên file trong docstring. Đối chiếu với bản trước khi gom, cùng một phép băm: số khoá i18n 1431 -> 1431 hash STRINGS a06cc34b... (trùng) QSS dark hash 7bb230a4... (trùng) QSS light hash 884f73ce... (trùng) `check_loc.py` phải khai thêm "i18n", "theme" vào DEFAULT_TARGET_DIRS: chúng từng được quét theo diện "module nằm ở thư mục gốc", gom vào gói rồi thì không khai là lặng lẽ tuột khỏi tầm quét. Bánh cóc `ui/widgets.py` siết 505 -> 466 sau khi tách SegmentedControl — nợ cũ co lại thì con số phải co theo, không thì bánh cóc đứng yên mãi ở mức cũ. Một file requirements --------------------- Xoá `requirements-test.txt`. Nó chỉ có `pytest` + `pydantic`, nhưng 64/108 file test dựng widget thật và 20 file trong đó import PySide6 thẳng ở đầu file không có bảo vệ — nên CI cài mỗi file kia thì pytest chết ngay lúc thu thập test chứ không phải "vài test bị bỏ qua". Hai file cho một danh sách gần trùng nhau chỉ tạo thêm một chỗ để lệch phiên bản, và `pydantic` đã bị chép ở cả hai. CI đổi sang cài `requirements.txt`. Người dùng cuối cài thừa pytest vài MB. Kèm theo: `install.bat` bỏ cờ `--dev` (không còn gì để cài thêm). Khối `if` rỗng còn sót lại làm cmd.exe báo "( was unexpected at this time" và script chết ngay sau bước cài thư viện — đã gỡ hẳn. 859 test xanh · 4/4 cổng CASAN · check_design_parity 32/32 · check_layout_geometry trùng từng byte với bản trước refactor. Co-Authored-By: Claude Opus 5 (1M context) --- .gitea/workflows/ci.yaml | 10 +++-- CONTRIBUTING.md | 2 +- README.md | 8 +++- i18n.py => i18n/__init__.py | 40 +++++++++---------- .../agents_admin_tab.py | 0 i18n_composer.py => i18n/composer.py | 0 i18n_cowork_tab.py => i18n/cowork_tab.py | 0 i18n_hint.py => i18n/hint.py | 0 .../libreoffice_view.py | 0 i18n_login_dialog.py => i18n/login_dialog.py | 0 .../monitoring_overview.py | 0 .../settings_dialog.py | 0 i18n_sidebar.py => i18n/sidebar.py | 0 .../skills_dialog.py | 0 install.bat | 15 +------ requirements-test.txt | 2 - requirements.txt | 9 +++++ scripts/check_loc.py | 6 ++- tests/test_project_context_mcp_template.py | 14 ++++--- theme.py => theme/__init__.py | 4 +- theme_palettes.py => theme/palettes.py | 0 theme_qss.py => theme/qss.py | 4 +- .../qss_controls.py | 6 +-- ui/segmented_control.py | 2 +- 24 files changed, 66 insertions(+), 56 deletions(-) rename i18n.py => i18n/__init__.py (77%) rename i18n_agents_admin_tab.py => i18n/agents_admin_tab.py (100%) rename i18n_composer.py => i18n/composer.py (100%) rename i18n_cowork_tab.py => i18n/cowork_tab.py (100%) rename i18n_hint.py => i18n/hint.py (100%) rename i18n_libreoffice_view.py => i18n/libreoffice_view.py (100%) rename i18n_login_dialog.py => i18n/login_dialog.py (100%) rename i18n_monitoring_overview.py => i18n/monitoring_overview.py (100%) rename i18n_settings_dialog.py => i18n/settings_dialog.py (100%) rename i18n_sidebar.py => i18n/sidebar.py (100%) rename i18n_skills_dialog.py => i18n/skills_dialog.py (100%) delete mode 100644 requirements-test.txt rename theme.py => theme/__init__.py (97%) rename theme_palettes.py => theme/palettes.py (100%) rename theme_qss.py => theme/qss.py (98%) rename theme_qss_controls.py => theme/qss_controls.py (98%) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index dce0038..8f68dce 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -34,10 +34,14 @@ jobs: with: python-version: "3.11" cache: pip - cache-dependency-path: cowork_local/requirements-test.txt + cache-dependency-path: cowork_local/requirements.txt - - name: Install test dependencies - run: python -m pip install --disable-pip-version-check -r requirements-test.txt + # Mot file duy nhat: requirements-test.txt cu chi co pytest, nhung + # 64/108 file test dung widget that (20 file import PySide6 thang o dau + # file, khong co bao ve) nen no van phai keo ve gan nhu ca danh sach + # runtime. Cai rieng file kia thi pytest chet ngay luc thu thap test. + - name: Install dependencies + run: python -m pip install --disable-pip-version-check -r requirements.txt - name: Check Python syntax run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e92ee4..fa49083 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ Prefer the existing lightweight Conventional Commit prefixes: `feat:`, `fix:`, ` Run the application from the parent directory with `python -m cowork_local`. The current reliable test command is: ```bash -python -m pip install -r requirements-test.txt +python -m pip install -r requirements.txt python -m pytest tests -q ``` diff --git a/README.md b/README.md index 469d9f8..f90b846 100644 --- a/README.md +++ b/README.md @@ -59,10 +59,16 @@ python -m cowork_local ### 3. Run Automated Tests ```bash -python -m pip install -r requirements-test.txt +python -m pip install -r requirements.txt pytest -q ``` +There is one requirements file, not a runtime/test pair. A separate test file +would hold only `pytest`: 64 of the 108 test modules build real widgets, and 20 +of them import PySide6 unguarded at module scope, so it would have to pull in +almost the whole runtime list anyway — two files for one near-identical list is +just a second place for the pins to drift. + --- ## 🛡️ CASAN Quality Gate & Verification diff --git a/i18n.py b/i18n/__init__.py similarity index 77% rename from i18n.py rename to i18n/__init__.py index 9f0267f..20f004a 100644 --- a/i18n.py +++ b/i18n/__init__.py @@ -27,30 +27,30 @@ _current = DEFAULT_LANGUAGE _listeners: List[Callable[[], None]] = [] # key -> {"en": ..., "ja": ..., "vi": ...} -from . import i18n_login_dialog as _i18n_login_dialog -from . import i18n_sidebar as _i18n_sidebar -from . import i18n_composer as _i18n_composer -from . import i18n_hint as _i18n_hint -from . import i18n_cowork_tab as _i18n_cowork_tab -from . import i18n_settings_dialog as _i18n_settings_dialog -from . import i18n_skills_dialog as _i18n_skills_dialog -from . import i18n_libreoffice_view as _i18n_libreoffice_view -from . import i18n_agents_admin_tab as _i18n_agents_admin_tab -from . import i18n_monitoring_overview as _i18n_monitoring_overview +from . import login_dialog as _login_dialog +from . import sidebar as _sidebar +from . import composer as _composer +from . import hint as _hint +from . import cowork_tab as _cowork_tab +from . import settings_dialog as _settings_dialog +from . import skills_dialog as _skills_dialog +from . import libreoffice_view as _libreoffice_view +from . import agents_admin_tab as _agents_admin_tab +from . import monitoring_overview as _monitoring_overview # Gộp theo đúng thứ tự cũ: khoá trùng thì cụm sau thắng, y như khi tất cả # còn nằm chung một dict literal. STRINGS: Dict[str, Dict[str, str]] = { - **_i18n_login_dialog.STRINGS, - **_i18n_sidebar.STRINGS, - **_i18n_composer.STRINGS, - **_i18n_hint.STRINGS, - **_i18n_cowork_tab.STRINGS, - **_i18n_settings_dialog.STRINGS, - **_i18n_skills_dialog.STRINGS, - **_i18n_libreoffice_view.STRINGS, - **_i18n_agents_admin_tab.STRINGS, - **_i18n_monitoring_overview.STRINGS, + **_login_dialog.STRINGS, + **_sidebar.STRINGS, + **_composer.STRINGS, + **_hint.STRINGS, + **_cowork_tab.STRINGS, + **_settings_dialog.STRINGS, + **_skills_dialog.STRINGS, + **_libreoffice_view.STRINGS, + **_agents_admin_tab.STRINGS, + **_monitoring_overview.STRINGS, } diff --git a/i18n_agents_admin_tab.py b/i18n/agents_admin_tab.py similarity index 100% rename from i18n_agents_admin_tab.py rename to i18n/agents_admin_tab.py diff --git a/i18n_composer.py b/i18n/composer.py similarity index 100% rename from i18n_composer.py rename to i18n/composer.py diff --git a/i18n_cowork_tab.py b/i18n/cowork_tab.py similarity index 100% rename from i18n_cowork_tab.py rename to i18n/cowork_tab.py diff --git a/i18n_hint.py b/i18n/hint.py similarity index 100% rename from i18n_hint.py rename to i18n/hint.py diff --git a/i18n_libreoffice_view.py b/i18n/libreoffice_view.py similarity index 100% rename from i18n_libreoffice_view.py rename to i18n/libreoffice_view.py diff --git a/i18n_login_dialog.py b/i18n/login_dialog.py similarity index 100% rename from i18n_login_dialog.py rename to i18n/login_dialog.py diff --git a/i18n_monitoring_overview.py b/i18n/monitoring_overview.py similarity index 100% rename from i18n_monitoring_overview.py rename to i18n/monitoring_overview.py diff --git a/i18n_settings_dialog.py b/i18n/settings_dialog.py similarity index 100% rename from i18n_settings_dialog.py rename to i18n/settings_dialog.py diff --git a/i18n_sidebar.py b/i18n/sidebar.py similarity index 100% rename from i18n_sidebar.py rename to i18n/sidebar.py diff --git a/i18n_skills_dialog.py b/i18n/skills_dialog.py similarity index 100% rename from i18n_skills_dialog.py rename to i18n/skills_dialog.py diff --git a/install.bat b/install.bat index c689691..cb1787a 100644 --- a/install.bat +++ b/install.bat @@ -4,7 +4,6 @@ rem Cowork-Local BamBOO - cai dat thu vien Python (chay MOT lan) rem rem Cach dung: rem install.bat cai vao moi truong ao rieng (khuyen dung) -rem install.bat --dev cai them thu vien de chay test rem install.bat --system cai thang vao Python dang co, khong dung venv rem install.bat --force dung lai moi truong ao tu dau rem @@ -26,13 +25,11 @@ set "APPHOME=%LOCALAPPDATA%\CoworkLocal" set "VENV=%APPHOME%\venv" set "LAUNCHER=%APPHOME%\launcher" -set "DEV=0" set "USE_SYSTEM=0" set "FORCE=0" :parse_args if "%~1"=="" goto args_done -if /I "%~1"=="--dev" set "DEV=1" & shift & goto parse_args if /I "%~1"=="--system" set "USE_SYSTEM=1" & shift & goto parse_args if /I "%~1"=="--force" set "FORCE=1" & shift & goto parse_args if /I "%~1"=="-h" goto usage @@ -120,15 +117,6 @@ if errorlevel 1 ( goto fail ) -if "%DEV%"=="1" ( - echo [3/5] Cài thêm thư viện chạy test ^(--dev^)... - %PIP% install --disable-pip-version-check -r "%REPO%\requirements-test.txt" - if errorlevel 1 ( - echo [LỖI] Cài thư viện test thất bại. - goto fail - ) -) - rem -------------------------------------------------------------------------- rem 4. Lien ket de goi import duoc dung ten rem @@ -188,9 +176,8 @@ exit /b 0 :usage echo. -echo install.bat [--dev] [--system] [--force] +echo install.bat [--system] [--force] echo. -echo --dev cài thêm thư viện để chạy test ^(pytest, pydantic^) echo --system cài thẳng vào Python đang có, không tạo môi trường ảo echo --force xoá môi trường ảo cũ rồi tạo lại từ đầu echo. diff --git a/requirements-test.txt b/requirements-test.txt deleted file mode 100644 index e3b9720..0000000 --- a/requirements-test.txt +++ /dev/null @@ -1,2 +0,0 @@ -pydantic>=2,<3 -pytest>=8,<10 diff --git a/requirements.txt b/requirements.txt index 3151a94..f25beab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,3 +39,12 @@ pywin32>=306; sys_platform == "win32" # Office -> PDF, thông báo Outlook # opendataloader-pdf # bộ đọc PDF thay thế — KHÔNG cài sẵn có chủ ý: # # application/workspaces/graph_index_service.py tự cài # # khi cần, qua core/deps.py::ensure_module. + +# --- Chạy test --- +# Gộp vào đây thay vì để riêng requirements-test.txt: file kia chỉ có đúng +# `pytest`, mà 64/108 file test dựng widget thật nên nó vẫn phải kéo về gần +# như toàn bộ danh sách trên. Hai file cho một danh sách gần trùng nhau chỉ +# tạo thêm một chỗ để lệch phiên bản. +# +# Người dùng cuối cài thừa pytest vài MB — đổi lại chỉ còn MỘT file phải nhớ. +pytest>=8,<10 diff --git a/scripts/check_loc.py b/scripts/check_loc.py index 03eebbd..4bdc5b6 100644 --- a/scripts/check_loc.py +++ b/scripts/check_loc.py @@ -40,6 +40,10 @@ if hasattr(sys.stdout, "reconfigure"): DEFAULT_TARGET_DIRS = [ "domain", "application", "infrastructure", "presentation", "ui", "core", "providers", "security", "mcp_servers", + # ``i18n/`` và ``theme/`` từng là 13 file rời nằm thẳng ở thư mục gốc nên + # được quét theo diện "module gốc"; gom vào gói rồi thì phải khai ở đây, + # không thì chúng lặng lẽ tuột khỏi tầm quét. + "i18n", "theme", ] DEFAULT_MAX_LINES = 400 @@ -60,7 +64,7 @@ SCAN_ROOT_MODULES = True #: đúng là tách file. LEGACY_ALLOWANCE = { "ui/workspace_tab.py": 566, - "ui/widgets.py": 505, + "ui/widgets.py": 466, "ui/task_editor_dialog.py": 627, "ui/accounts_tab.py": 559, "core/skills.py": 405, diff --git a/tests/test_project_context_mcp_template.py b/tests/test_project_context_mcp_template.py index b88f435..55ca6d1 100644 --- a/tests/test_project_context_mcp_template.py +++ b/tests/test_project_context_mcp_template.py @@ -87,12 +87,14 @@ def source() -> dict[str, str]: def test_template_exposes_exactly_three_provider_neutral_tools() -> None: - # The MCP SDK is a RUNTIME dependency (requirements.txt) and is deliberately - # absent from requirements-test.txt, which is all CI installs. Importing it at - # module scope aborted collection for the ENTIRE suite, so the guard lives here, - # inside the only test that touches the SDK. Guarding per-test rather than - # per-module keeps the other cases -- pure-Python contract checks that need no - # SDK -- running on CI instead of silently skipping with it. + # Importing the MCP SDK at module scope aborted collection for the ENTIRE + # suite whenever the SDK was missing, so the guard lives here, inside the only + # test that touches it. Guarding per-test rather than per-module keeps the + # other cases -- pure-Python contract checks that need no SDK -- running + # instead of silently skipping with it. + # + # ``mcp`` is in requirements.txt, so a correctly installed checkout runs this + # test for real; the guard only covers an environment installed by hand. types = pytest.importorskip("mcp.types") assert set(TOOL_NAMES) == EXPECTED_TOOLS diff --git a/theme.py b/theme/__init__.py similarity index 97% rename from theme.py rename to theme/__init__.py index 21ef8d4..2adc35d 100644 --- a/theme.py +++ b/theme/__init__.py @@ -34,10 +34,10 @@ original value, so the deviation is auditable rather than silent. """ from __future__ import annotations -from .theme_palettes import ( # noqa: F401 — giữ đường vào cũ +from .palettes import ( # noqa: F401 — giữ đường vào cũ DARK, LIGHT, Palette, _chevron_asset, _FONT, _MONO, _PALETTES, ) -from .theme_qss import _TEMPLATE +from .qss import _TEMPLATE from dataclasses import dataclass, asdict from string import Template diff --git a/theme_palettes.py b/theme/palettes.py similarity index 100% rename from theme_palettes.py rename to theme/palettes.py diff --git a/theme_qss.py b/theme/qss.py similarity index 98% rename from theme_qss.py rename to theme/qss.py index 6e6c753..ee76110 100644 --- a/theme_qss.py +++ b/theme/qss.py @@ -4,7 +4,7 @@ Tách khỏi ``theme.py`` vì nó là **dữ liệu**, không phải logic: mộ ``string.Template`` mà ``stylesheet()`` thay biến vào. Để chung thì mỗi lần muốn sửa một hàm nhỏ trong theme.py lại phải cuộn qua 470 dòng CSS. -Sửa màu thì sang ``theme_palettes.py``; ở đây chỉ sửa hình dạng và khoảng cách. +Sửa màu thì sang ``theme/palettes.py``; ở đây chỉ sửa hình dạng và khoảng cách. """ from __future__ import annotations @@ -12,7 +12,7 @@ from dataclasses import dataclass, asdict from string import Template -from .theme_qss_controls import QSS_CONTROLS +from .qss_controls import QSS_CONTROLS _QSS_SHELL = """ /* ---- reset ------------------------------------------------------------ */ diff --git a/theme_qss_controls.py b/theme/qss_controls.py similarity index 98% rename from theme_qss_controls.py rename to theme/qss_controls.py index b8b0075..2f89ebf 100644 --- a/theme_qss_controls.py +++ b/theme/qss_controls.py @@ -1,10 +1,10 @@ """Nửa sau của khuôn QSS: bề mặt, tab, ô nhập, nút, badge, log. -Cắt đôi khuôn QSS đúng mạch của chính nó: ``theme_qss.py`` giữ phần vỏ +Cắt đôi khuôn QSS đúng mạch của chính nó: ``theme/qss.py`` giữ phần vỏ (reset + shell: thanh rail, khung chính), file này giữ phần điều khiển. -Hai nửa được nối lại trong ``theme_qss.py``. +Hai nửa được nối lại trong ``theme/qss.py``. -Sửa màu thì sang ``theme_palettes.py``; ở đây chỉ sửa hình dạng và khoảng cách. +Sửa màu thì sang ``theme/palettes.py``; ở đây chỉ sửa hình dạng và khoảng cách. """ from __future__ import annotations diff --git a/ui/segmented_control.py b/ui/segmented_control.py index 6b8df2d..46a04f5 100644 --- a/ui/segmented_control.py +++ b/ui/segmented_control.py @@ -26,7 +26,7 @@ class SegmentedControl(QWidget): currentIndexChanged = Signal(int) - #: Độ đậm mà ``theme_qss.py`` áp cho nút đang chọn + #: Độ đậm mà ``theme/qss.py`` áp cho nút đang chọn #: (``QPushButton#segItem:checked { font-weight: 600 }``). Đổi ở QSS thì #: phải đổi cả ở đây, nếu không chữ lại bị cắt. _CHECKED_WEIGHT = QFont.DemiBold